Chapter 31. 测试Seam应用程序

大部分的Seam应用程序至少需要两种类型的自动测试: 单元测试(unit test) 是隔离测试特定的Seam组件,和脚本化的 集成测试(integration test) 是综合地测试应用中所有的Java层面(即除了表现层之外的所有内容)。

两种类型的测试都很容易编写。

31.1. Seam组件的单元测试

所有的Seam组件都是简单Java对象(POJO)。如果你想简化单元测试,那么这是个极好的开端,而且Seam将其重点放在组件间交互的双向注入和上下文对象的访问上,这使得Seam的组件在脱离其正常运行环境时,也可以很容易地被测试。

思考如下的Seam组件:

@Stateless
@Scope(EVENT)
@Name("register")
public class RegisterAction implements Register
{
   private User user;
   private EntityManager em;

   @In
   public void setUser(User user) {
       this.user = user;
   }

   @PersistenceContext
   public void setBookingDatabase(EntityManager em) {
       this.em = em;
   }

   public String register()
   {
      List existing = em.createQuery("select username from User where username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();
      if (existing.size()==0)
      {
         em.persist(user);
         return "success";
      }
      else
      {
         return null;
      }
   }

}

测试上述组件的TestNG测试如下:

public class RegisterActionTest
{

    @Test
    public testRegisterAction()
    {
        EntityManager em = getEntityManagerFactory().createEntityManager();
        em.getTransaction().begin();

        User gavin = new User();
        gavin.setName("Gavin King");
        gavin.setUserName("1ovthafew");
        gavin.setPassword("secret");

        RegisterAction action = new RegisterAction();
        action.setUser(gavin);
        action.setBookingDatabase(em);

        assert "success".equals( action.register() );

        em.getTransaction().commit();
        em.close();
    }


    private EntityManagerFactory emf;

    public EntityManagerFactory getEntityManagerFactory()
    {
        return emf;
    }

    @BeforeClass
    public void init()
    {
        emf = Persistence.createEntityManagerFactory("myResourceLocalEntityManager");
    }

    @AfterClass
    public void destroy()
    {
        emf.close();
    }
    
}

Java Persistence API可以与Java SE和Java EE一起使用 — 当上述组件在应用服务器(Java EE)中使用时,由容器来负责事务管理;然而,在单元测试(Java SE)里,事务必须显式地使用本地资源实体管理器来进行管理。 这要求在 persistence.xml 进行配置。

Seam的组件通常不直接依赖于容器的基础设施,因此大部分的单元测试跟上述一样容易。

31.2. Seam组件的集成测试

相比单元测试,集成测试有稍许的难度。在这里,我们不能再对容器的基础设施视而不见,相反这也正是需要测试的一部分! 同时,我们也不想强制地将我们的应用程序部署到应用服务器上来运行这些自动化测试。 那么为了能全面地测试我们的应用程序,且在不损失太多性能的条件下,我们需要在测试环境中再造必要的容器基础设施。

因此Seam采取的方法是在一个经修剪过的容器环境中(Seam,以及嵌入式的JBoss容器,需要JDK 1.5并且不支持JDK 1.6)编写测试用例来测试你的组件。

public class RegisterTest extends SeamTest
{

   @Test
   public void testRegisterComponent() throws Exception
   {

      new ComponentTest() {

         protected void testComponents() throws Exception
         {
            setValue("#{user.username}", "1ovthafew");
            setValue("#{user.name}", "Gavin King");
            setValue("#{user.password}", "secret");
            assert invokeMethod("#{register.register}").equals("success");
            assert getValue("#{user.username}").equals("1ovthafew");
            assert getValue("#{user.name}").equals("Gavin King");
            assert getValue("#{user.password}").equals("secret");
         }

      }.run();

   }

   ...

}

31.2.1. 在集成测试中使用Mock对象

有时候,Seam的一些组件所依赖的资源在集成测试环境中没有,那么我们需要替换这些组件。 例如,假设现在有一些Seam组件,他们是对支付处理系统的facade,示例如下

@Name("paymentProcessor")
public class PaymentProcessor {
    public boolean processPayment(Payment payment) { .... }
}

为了能够集成测试,我们可以对此组件Mock如下:

@Name("paymentProcessor")
@Install(precedence=MOCK)
public class MockPaymentProcessor extends PaymentProcessor {
    public void processPayment(Payment payment) {
        return true;
    }
}

因为 MOCK 的优先级比应用组件的默认优先级要高,所以Seam将优先装配在classpath中的Mock对象。当部署到生产环境中的时候,那些Mock对象将不复存在,因此真正的组件将被装配进来。

31.3. 集成测试Seam应用程序中的用户交互

在测试中,一个更难的问题是模拟用户交互。因此第三个问题是:我们应该在那里放置断言(assertion)。 一些测试框架通过在Web浏览器中重现用户交互来测试整个应用程序,这些测试有其适用之处,但他们并不适合在开发时使用。

在一个模拟的JSF环境中,SeamTest 可以让你编写 脚本化(scripted) 测试。 这些脚本化测试的用处是为了重现视图和Seam组件之间的交互,换句话说,你要假装你是JSF的实现!

这种方法可以测试除了视图以外的所有事物。

让我们来看一个JSP视图,此视图对应的组件就是上述单元测试过那个组件:

<html>
 <head>
  <title>Register New User</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <table border="0">
       <tr>
         <td>Username</td>
         <td><h:inputText value="#{user.username}"/></td>
       </tr>
       <tr>
         <td>Real Name</td>
         <td><h:inputText value="#{user.name}"/></td>
       </tr>
       <tr>
         <td>Password</td>
         <td><h:inputSecret value="#{user.password}"/></td>
       </tr>
     </table>
     <h:messages/>
     <h:commandButton type="submit" value="Register" action="#{register.register}"/>
   </h:form>
  </f:view>
 </body>
</html>

我们想测试一下应用程序的注册功能(即当用户点击注册按钮要发生的事情)。我们可以在TestNG的自动测试中重现JSF的请求生命周期:

public class RegisterTest extends SeamTest
{

   @Test
   public void testRegister() throws Exception
   {

      new FacesRequest() {

         @Override
         protected void processValidations() throws Exception
         {
            validateValue("#{user.username}", "1ovthafew");
            validateValue("#{user.name}", "Gavin King");
            validateValue("#{user.password}", "secret");
            assert !isValidationFailure();
         }

         @Override
         protected void updateModelValues() throws Exception
         {
            setValue("#{user.username}", "1ovthafew");
            setValue("#{user.name}", "Gavin King");
            setValue("#{user.password}", "secret");
         }

         @Override
         protected void invokeApplication()
         {
            assert invokeMethod("#{register.register}").equals("success");
         }

         @Override
         protected void renderResponse()
         {
            assert getValue("#{user.username}").equals("1ovthafew");
            assert getValue("#{user.name}").equals("Gavin King");
            assert getValue("#{user.password}").equals("secret");
         }

      }.run();

   }

   ...

}

值得注意的是:我们继承了 SeamTest,其为我们的组件提供了一个Seam环境,并且我们还需要写一个继承了 SeamTest.FacesRequest 的匿名类,此匿名类模拟JSF的请求生命周期(还有一个 SeamTest.NonFacesRequest 是测试GET请求的)。 为了模拟JSF对我们组件的调用,我们已经完成了JSF不同阶段的方法实现,接着我们还加入了各种断言。

你可以在Seam的更复杂的示例应用程序中找到大量关于集成测试的用法,还有在Ant或者Eclipse的TestNG插件下运行这些测试的使用说明。

31.3.1. 利用Mock数据进行集成测试

如果你需要在每个测试之前在数据库中插入或清除数据,你可以使用DBUnit进行Seam的集成测试。要做到这一点,要继承DBUnitSeamTest而不是SeamTest。

你需要提供数据集给DBUnit:

<dataset>

   <ARTIST
      id="1"
      dtype="Band"
      name="Pink Floyd" />

   <DISC
      id="1"
      name="Dark Side of the Moon"
      artist_id="1" />

</dataset>

并通过覆盖 prepareDBUnitOperations() 来告诉Seam:

protected void prepareDBUnitOperations() {
    beforeTestOperations.add(
       new DataSetOperation("my/datasets/BaseData.xml")
    );
 }

如果没有指定其它的操作作为构造器参数 DataSetOperation 的操作默认是 DatabaseOperation.CLEAN_INSERT。 在调用每个 @Test 方法前,上述的示例会先清除 BaseData.xml 中定义的所有的表,然后插入 BaseData.xml 中定义的所有的数据行。

如果你需要在一个测试方法执行后进行额外的清除工作,添加操作到 afterTestOperations 列表中。

你需要通过设置一个名为 datasourceJndiName 的TestNG测试参数来告诉DBUnit你正在使用的数据源:

   <parameter name="datasourceJndiName" value="java:/seamdiscsDatasource"/>
         

31.3.2. Seam Mail集成测试

警告!这个功能仍在开发当中。

集成测试Seam Mail相当的简单:

public class MailTest extends SeamTest {

   @Test
   public void testSimpleMessage() throws Exception {

      new FacesRequest() {

         @Override
         protected void updateModelValues() throws Exception {
            setValue("#{person.firstname}", "Pete");
            setValue("#{person.lastname}", "Muir");
            setValue("#{person.address}", "[email protected]");
         }

         @Override
         protected void invokeApplication() throws Exception {
            MimeMessage renderedMessage = getRenderedMailMessage("/simple.xhtml");
            assert renderedMessage.getAllRecipients().length == 1;
            InternetAddress to = (InternetAddress) renderedMessage.getAllRecipients()[0];
            assert to.getAddress().equals("[email protected]");
         }

      }.run();
   }
}

我们与往常一样创建一个新的 FacesRequest。 在 invokeApplication 里我们通过传递消息的viewId去渲染 getRenderedMailMessage(viewId); 的消息。 这个方法返回已经渲染完成的消息,你可以继续进行你的测试。你当然可以同时使用任何一项标准JSF的生命周期的方法。

还有就是不支持渲染标准JSF组件,所以你不能方便地测试邮件消息的内容主体。