Play framework の Tutorial を Scala と Scalate で行う(Step1, Step2)
Scala を使い始めて Java 文化*1に辟易している方は絶対に使った方が良い Play framework の Tutorial を行ってみたので、そのメモを残す。Play framework は、1.1 の beta version がインストール済みであると仮定する。
Tutorial を始める前の下準備
Play framework で Scala と Scalate を使うためにモジュールをインストールする。
% sudo play install scala % sudo play install scalate
OSX 環境で下記のエラーが出る場合は、環境設定から Proxy を外すなどで対処。10.5 から 10.6 へ更新した MBP では出ないが、10.6 が始めから入っていた Macmini では出た。
File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/urllib.py", line 1425, in proxy_bypass_macosx_sysconf mask = int(m.group(2)[1:])
ちなみに play コマンドは Java ではなく Python で記述されているので、とても軽快。重量級の sbt から解放されるので幸せ。
Starting up the project (Scala version)
http://www.playframework.org/documentation/1.1-trunk/scguide1
Tutorial の通りに行うと scalate が使われないので下記のようにする。
% play new yabe --with scala,scalate
その後、app/controllers.scala の Application オブジェクトが継承している Controller を ScalateController に変更する。これを怠ると Controller の render メソッドが実行され index.html が存在しないと怒られる。今回は scalate を使いたいので index.ssp を読み込んで欲しい。
//..snip.. object Application extends ScalateController { //..snip..
後は、Tutorial の通りに進めるだけ。
A first iteration for the data model (Scala version)
http://www.playframework.org/documentation/1.1-trunk/scguide2
User モデルとテストの作成で幾つかコンパイルエラーになる箇所があるため、その修正を行う必要がある。
createAndRetrieveUser
app/models/User.scala の下記は削除。
import javax.persistence._
また、後に Users オブジェクトの find メソッドを使うので app/models/User.scala には下記を追加しておく。
object Users extends QueryOn[User]
test/BasicTest.scala の下記の行を…
val bob = find[User]("byEmail", "bob@gmail.com").first
下記のように修正する。
var bob = Users.find("byEmail", "bob@gmail.com").first
Users.find の戻り値は Option クラス(ようは Some か None)であるため、下記を…
assertEquals("Bob", bob.fullname)
下記に変更する必要がある。
assertEquals("Bob", bob.get.fullname)
tryConnectAsUser
app/models/User.scala で下記をインポートし User オブジェクトを作成するよう指示があるが、これは無視。
import play.db.jpa.QueryFunctions._
前述で追加した Users オブジェクトに connect メソッドを追加する。
object Users extends QueryOn[User] { def connect(email: String, password: String) = { find("byEmailAndPassword", email, password).first } }
test/BasicTest.scala の tryConnectAsUser は下記のようにする。assertNotEquals とか欲しいなぁ。
@Test def tryConnectAsUser() { // Create a new user and save it new User("bob@gmail.com", "secret", "Bob").save() // Test assertEquals("Bob", Users.connect("bob@gmail.com", "secret").get.fullname) assertEquals(None, Users.connect("bob@gmail.com", "badpassword")) assertEquals(None, Users.connect("tom@gmail.com", "secret")) }
ScalaTest
ここまで書いて、Play Scala Module の Yabe サンプルが ScalaTest である事に気がつく。test/BasicTest.scala を下記のようにする。
import play.test._ import models._ import org.scalatest.matchers.ShouldMatchers class BasicTest extends UnitFlatSpec with ShouldMatchers { it should "create and retrieve a user" in { // Create a new user and save it new User("bob@gmail.com", "secret", "Bob").save() // Retrieve the user with bob username var bob = Users.find("byEmail", "bob@gmail.com").first // Test bob should not be (None) "Bob" should equal (bob.get.fullname) } it should "call connect on User" in { // Create a new user and save it new User("bob@gmail.com", "secret", "Bob").save() // Test Users.connect("bob@gmail.com", "secret") should not be (None) Users.connect("bob@gmail.com", "badpassword") should be (None) Users.connect("tom@gmail.com", "secret") should be (None) } }
こっちの方が、None か否かハッキリしてて良い。
createPost
app/models/Post.scala は、app/models/User.scala 同様に import を削除し、下記のような Posts オブジェクトを追加しておく。他は Tutorial のままで良い。
object Posts extends QueryOn[Post]
追加で個人的な趣味で「import java.util._」ではなく「import java.util.Date」とした。
ScalaTest に切り替えたので test/BasicTest.scala の Fixtures.deleteAll() の箇所は下記のようにする。
//..snip.. import org.scalatest.BeforeAndAfterEach //..snip.. class BasicTest extends UnitFlatSpec with ShouldMatchers with BeforeAndAfterEach { override def beforeEach() { Fixtures.deleteAll() }
beforeEach は、org.scalatest.BeforeAndAfterEach のメソッドであるため忘れずに継承しておく。
テストケースは下記の通り。
it should "create Post" in { // Create a new user and save it var bob = new User("bob@gmail.com", "secret", "Bob").save() // Create a new post new Post(bob, "My first post", "Hello world").save() // Test that the post has been created 1 should equal (Posts.count()) // Retrieve all post created by bob var bobPosts = Posts.find("byAuthor", bob).fetch // Tests 1 should equal (bobPosts.size) var firstPost = bobPosts(0) firstPost should not be (null) bob should equal (firstPost.author) "My first post" should equal (firstPost.title) "Hello world" should equal (firstPost.content) firstPost.postedAt should not be (null) }
postComments
app/models/Comment.scala は他のモデルと注意点が同じであるため説明省略。Comments オブジェクトの追加だけは忘れずに。
テストケースは下記の通り。
it should "post Comments" in { // Create a new user and save it var bob = new User("bob@gmail.com", "secret", "Bob").save() // Create a new post var bobPost = new Post(bob, "My first post", "Hello world").save() // Post a first comment new Comment(bobPost, "Jeff", "Nice post").save() new Comment(bobPost, "Tom", "I knew that !").save() // Retrieve all comments var bobPostComments = Comments.find("byPost", bobPost).fetch // Tests 2 should equal (bobPostComments.size) var firstComment = bobPostComments(0) firstComment should not be (null) "Jeff" should equal ( firstComment.author) "Nice post" should equal ( firstComment.content) firstComment.postedAt should not be (null) var secondComment = bobPostComments(1) secondComment should not be (null) "Tom" should equal ( secondComment.author) "I knew that !" should equal (secondComment.content) secondComment.postedAt should not be (null) }
ここで、tmp の下に Comment.class が作成されずにエラーが出てしまったので「play test」を再実行している。
useTheCommentsRelation
app/models/Post.scala を下記のように修正する。
package models import java.util.{Date, List=>JList, ArrayList} import play.db.jpa._ @Entity class Post( @ManyToOne var author: User, var title: String, @Lob var content: String ) extends Model { var postedAt: Date = new Date() def this() = this(null, null, null) @OneToMany(mappedBy="post", cascade=Array(CascadeType.ALL)) var comments: JList[Comment] = new ArrayList[Comment] def addComment(author: String, content: String) = { val newComment = new Comment(this, author, content) newComment.save() comments.add(newComment) this } } object Posts extends QueryOn[Post]
テストケースは下記の通り。
//..snip.. import scala.collection.JavaConversions._ //..snip.. it should "use the comments relation" in { // Create a new user and save it var bob = new User("bob@gmail.com", "secret", "Bob").save() // Create a new post var bobPost = new Post(bob, "My first post", "Hello world").save() // Post a first comment bobPost.addComment("Jeff", "Nice post") bobPost.addComment("Tom", "I knew that !") // Count things 1 should equal (Users.count()) 1 should equal (Posts.count()) 2 should equal (Comments.count()) // Retrieve the bob post val getBobPost = Posts.find("byAuthor", bob).first getBobPost should not be (None) // Navigate to comments 2 should equal (getBobPost.get.comments.size) "Jeff" should equal (getBobPost.get.comments(0).author) // Delete the post getBobPost.get.delete() // Chech the all comments have been deleted 1 should equal (Users.count()) 0 should equal (Posts.count()) 0 should equal (Comments.count()) }
scala.collection.JavaConversions を import しておかないと「getBobPost.get.comments(0)」の箇所で java.util.List が、そのまま使用されてしまうので気をつける。
ここは、Model 側でどうにかしたいので、後で考える。
fullTest
テストケースは下記の通り。
it should "work if things combined together" in { Fixtures.load("data.yml") // Count things 2 should equal (Users.count()) 3 should equal (Posts.count()) 3 should equal (Comments.count()) // Try to connect as users Users.connect("bob@gmail.com", "secret") should not be (None) Users.connect("jeff@gmail.com", "secret") should not be (None) Users.connect("jeff@gmail.com", "badpassword") should be (None) Users.connect("tom@gmail.com", "secret") should be (None) // Find all bob posts var bobPosts = Posts.find("author.email", "bob@gmail.com").fetch 2 should equal (bobPosts.size) // Find all comments related to bob posts var bobComments = Comments.find("post.author.email", "bob@gmail.com").fetch 3 should equal (bobComments.size) // Find the most recent post var frontPost = Posts.find("order by postedAt desc").first frontPost should not be (None) "About the model layer" should equal (frontPost.get.title) // Check that this post has two comments 2 should equal (frontPost.get.comments.size) // Post a new comment frontPost.get.addComment("Jim", "Hello guys") 3 should equal (frontPost.get.comments.size) 4 should equal (Comments.count()) }