2010/02/13

Play!(3)

從上次的教學我們已經能夠瞭解到MVC的基本部份了。這次我們說明多個MODEL的互相連帶處理「一對多」「多對一」和使用
JUnit來測試Applicaiton.

Model的連帶處理考量

從上回簡單的訊息表示Model(MsgData),這次我們再加上一個新的使用者情報Model(PersonData),這樣子顯示訊息時
不僅可以顯示使用者名稱,通常是使用SQL的JOIN句子,來互相關連,不過在Play!裡是使用JPA,Data並不是SQL table,
都是作為物件永續化管理,Model的關聯也是被設計成物件和物件的互相關聯。

首先在「models」裡作成PersonData類別

package models;

import java.util.*;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import play.db.jpa.Model;

@Entity
public class PersonData extends Model {
        public String name;
        public String mail;
        @OneToMany
        public List<MsgData> messages;
        
        public PersonData(String name,String mail){
                this.name = name;
                this.mail = mail;
                this.messages = new ArrayList<MsgData>();
        }

}

在這裡除了name和mail兩個欄位以外,還有保管MsgData實例的List欄位,這個List就是用來整合關聯MsgData物件。
這裡一定要注意的是「@OneToManay宣告」,JPA裡物件的永續化時,如果沒加上@OneToMany的話,就無法達成永續化。
藉由加上@OneToManay,這就告訴JPA這個欄位成了一對多對應的物件參照欄位。

MsgData的修正

接下來MsgData裡使用PersonData來取代name

package models;

import java.util.Calendar;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import play.db.jpa.Model;

@Entity
public class MsgData extends Model {
        private static final long serialVersionUID = 1L;
        public String message;
        @ManyToOne
        public PersonData person;
        public Date time = Calendar.getInstance().getTime();
        
        public MsgData(String message,PersonData person){
                this.person = person;
                this.message = message;
                person.messages.add(this);
        }
}

在這裡面有保管PersonData實例的person欄位,並加上了「@ManyToOne」的宣告,這樣一來就能夠將MsgData永續化。
new的時候,傳入引數裡的PersonData實例,在設定person屬性的同時,呼叫出person的message.add,在person
這邊將訊息保管起來。

PersonData的Controller

接下來作成PersonData的Controller,在「controller」資料夾裡做一個新的「Person.java」檔案。
這次就先加入add和index兩個基本的方法。

package controllers;

import java.util.List;
import play.mvc.Controller;
import models.*;

public class Person extends Controller {

        public static void index(String name) {
                List<PersonData> datas = null;
                datas = PersonData.findAll();
                render(datas);
        }
        
        public static void add(String name,String mail){
                String msg = "請輸入資料。";
                if (request.method.equals("POST")) {
                        PersonData person = new PersonData(name,mail);
                        person.save();
                        msg = "資料已經被保存了";
                }
                render(msg);
        }
}

準備PersonData用的Template

接下來在「views」資料夾裡做一個新的「Person」資料夾,然後加入兩個檔案「index.html」「add.html」

#index.html的程式碼
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>表示訊息</h1>
#{list items:datas, as:'data'}
    <li>${data.id}: ${data.name}(${data.mail})</li>
#{/list}

#add.html的程式碼
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>作成訊息</h1>
${msg}
<form method="post" action="@{Person.add}">
<table>
        <tr><td>name:</td><td><input type="text" id="name" name="name" /></td></tr>
        <tr><td>mail:</td><td><input type="text" id="mail" name="mail" /></td></tr>
        <tr><td></td><td><input type="submit" value="送出" /></td></tr>
</table>
</form>

接下來修正MsgData的Controller

package controllers;

import java.util.List;
import models.*;
import play.mvc.Controller;


public class Application extends Controller {
        
        public static void index(String name) {
                List<MsgData> datas = null;
                datas = MsgData.findAll();
                render(datas);
        }
        
        public static void add(String message,String name){
                String msg = "請輸入資料。";
                if (request.method.equals("POST")) {
                        PersonData person = PersonData.find("name",name).first();
                        if (person != null){
                                MsgData data = new MsgData(message,person);
                                data.save();
                                person.save();
                                msg = "資料已經被保存了。";
                        } else {
                                msg = "個人資料尚未登入。";
                        }
                }
                render(msg);
        }

}

作成MsgData用的Template

#index.html的原始碼
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>表示訊息</h1>
#{list items:datas, as:'data'}
    <li>${data.id}: ${data.message}(${data.person.name}, ${data.person.mail})</li>
#{/list}

#add.html的原始碼
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>作成訊息</h1>
${msg}
<form method="post" action="@{Application.add}">
<table>
        <tr><td>name:</td><td><input type="text" id="name" name="name" /></td></tr>
        <tr><td>message:</td><td><input type="text" id="message" name="message" /></td></tr>
        <tr><td></td><td><input id="submit" type="submit" value="送出" /></td></tr>
</table>
</form>

顯示使用者投稿的全部訊息

從MsgData裡取得PersonData的@ManyToOne處理已經明白了之後。接下來利用看看@OneToMany關聯的實例。
稍微修正剛才Person的index.html,將它改成可以顯示所有使用者的訊息。

#{extends 'main.html' /}
#{set title:'Home' /}
<h1>表示訊息</h1>
#{list items:datas, as:'data'}
        <li>${data.id}: ${data.name}(${data.mail})
        <ul>
        #{list items:data.messages, as:'message'}
                <li>${message.message}</li>
        #{/list}
</ul>
</li>
#{/list}

完全不需要改動到Controller,在#{list items:datas, as:'data'}的重複操作中,又重複操作
#{list items:data.messages, as:'message'}來取出PersonData裡的messages全部元素。

有關測試

在Play!裡備有JUnit的標準測試機能,在作成的專案裡都會有「test」這個資料夾,這個資料夾就是為了要配置測試用的
程式碼的地方。裡面有標準的「ApplicationTest.java」「BasicTest.java」「Application.test.html」等名稱
的程式碼檔案。這是在專案作成時自動生成的測試用程式碼,當然也可以修正這些檔案來做自己想要的測試。

在Play!裡有三種測試種類。「UnitTest(單元測試)」「FunctionalTest(功能測試)」「SeleniumTest」。
想要實行測試模式時,只要輸入

play test sampleapp

然後測試下面的網址

http://localhost:9000/@tests

檢查UnitTest程式碼

首先先看實行UnitTest的「BasicTest.java」檔案

import org.junit.*;
import java.util.*;
import play.test.*;
import models.*;

public class BasicTest extends UnitTest {

    @Test
    public void aVeryImportantThingToTest() {
        assertEquals(2, 1 + 1);
    }

}

在這裡「assertEquals」方法傳入兩個引數比較是否相等。這個UnitTest類別是直接繼承JUnit的org.junit.Assert類別,
並直接使用JUnit的assert方法。

檢查FunctionalTest程式碼

import org.junit.*;
import play.test.*;
import play.mvc.*;
import play.mvc.Http.*;
import models.*;

public class ApplicationTest extends FunctionalTest {

    @Test
    public void testThatIndexPageWorks() {
        Response response = GET("/");
        assertIsOk(response);
        assertContentType("text/html", response);
        assertCharset("utf-8", response);
    }
    
}

接下來在FunctionalTest裡加上一些各自的定義。頁面的存取已經在上面的範例測試過了,這次就定義測試Model的方法。

@Before
public void setup() {
        Fixtures.deleteAll();
}

@Test
public void testModelData() {
        PersonData p = new PersonData("測試", "test@test.com");
        p.save();
        MsgData m = new MsgData("這是測試。", p);
        m.save();
        p.messages.add(m);
        p.save();
        assertNotNull(p);
        assertNotNull(m);
        assertEquals(p.name, m.person.name);
        assertEquals(m.id,p.messages.get(0).id);
        List<PersonData> plist = PersonData.find("name", "測試").fetch();
        assertEquals(1, plist.size());
        List<MsgData> mlist = MsgData.find("person", plist.get(0)).fetch();
        assertEquals(1, mlist.size());
}

在這裡作成PersonData和MsgData,首先作成PersonData實例並保存,接下來將訊息和PersonData作為
引數作成MsgData實例,然後確認p和m的依存關係。assertNotNull確認引數是不是null。
assertEquals確認PersonData和MsgData的相同參照物件的name和id是否是參照到正確的物件。

這個測試之前,有一個setup的方法,並使用了「@Before」的宣告,這是表示說這是在測試前的前置動作,這裡用「Fixtures.deleteAll();」
就是將之前測試用的資料全部刪除、在每一個測試開始之前基本上都要先像這樣將資料初始化。

透過data.yml來生成測試資料

如果像之前生成資料、確認資料都還要寫一堆程式碼,在生成大量資料的時候就變得很麻煩。而且當要改變資料內容來驗證時
又還需要重新更改程式碼,如果可以的話,希望能夠把Model和程式碼切離。

在Play!的情況,「test」資料夾裡有一個「data.yml」檔案,紀錄了Model相關的情報,藉由這個來自動生成資料是可能的。

PersonData(tuyano):
  name: tuyano
  mail: tuyano@mac.com

PersonData(taro):
  name: taro
  mail: taro@yamada.com

MsgData(msg1):
  message: Hello
  person: tuyano

MsgData(msg2):
  message: Good-bye
  person: taro

把data.yml打開,然後以這樣的格式紀錄Model內容

Model名稱 ( 實例名稱 ):
屬性名稱: 値
屬性名稱: 値
……以下略……

測試用的程式碼

@Test
public void testFullData() {
        Fixtures.load("data.yml");
        assertEquals(2, PersonData.count());
        assertEquals(2, MsgData.count());
}

SeleniumTest的原始碼

SeleniumTest是在以HTML為基礎的Template檔案裡嵌入特殊的標籤來實行的。打開「Application.test.html」就能知道了。

#{selenium}
        open('/')
        waitForPageToLoad(1000)
        assertNotTitle('Application error')
#{/selenium}

使用seleniumTest測試add頁面

在Application.test.html裡加入

#{fixture delete:'all', load:'data.yml' /}
#{selenium}
        open('/add')
        waitForPageToLoad(1000)
        type('name','test')
        type('message','*** this is selenium test.***')
        clickAndWait('submit')
#{/selenium}

參考文章:http://codezine.jp/article/detail/4752

No comments:

Post a Comment