2011年12月8日 星期四

Parameterized Test in JUnit 4.10

最近準備練習使用 OpenRules 這套 business decision management system,因此同事找題目讓大家練習:How are ABO alleles inherited by our children? 答案請參考 http://www.bloodbook.com/inherited.html

Q: 如何用程式表達答案呢?

A: 根據前輩的教誨,我們要對 interface 寫程式,不要對 class 寫程式。因此這個問題的 interface 如下:
package main;

import java.util.Set;

public interface BloodTypeCalculator {
    Set<BloodType> calculate(BloodType father, BloodType mother);
    
    Set<BloodType> calculate(BloodType grandfather,    
                             BloodType grandmother,
                             BloodType maternalGrandfather, 
                             BloodType maternalGrandmother);
}
BloodType 是簡單的 Java enum type: O, A, B, AB. BloodTypeCalculator 提供兩個 methods:
  1. 根據父母的血型計算小孩可能的血型
  2. 根據父母的父母的血型計算小孩可能的血型

好,那要怎麼測 BloodTypeCalculator interface? 這個問題非常難回答,我的想法是:
直接寫 test class,讓 ctor 可以接收 BloodTypeCalculator interface pointer (in the sense of C++). 這樣就可以了。
但這樣不足以成事,如果沒有適當設計 test class,JUnit 不支援吃參數的 ctor。解決辦法就是 parameterized test. 不囉唆,直接貼程式碼:
package test;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import junit.framework.Assert;

import main.BloodType;
import main.BloodTypeCalculator;
import main.NativeBloodTypeCalculator;
import main.OpenRulesBloodTypeCalculator;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(value = Parameterized.class)
public class BloodTypeCalculatorTest {

    public BloodTypeCalculatorTest(BloodTypeCalculator c) {
        this.c = c;
    }
    
    @Parameters
    public static Collection<Object[]> parameters() {
        Object[][] result = new Object[][] { 
            { new NativeBloodTypeCalculator() }, 
            { new OpenRulesBloodTypeCalculator() }
        };
        return Arrays.asList(result);
    }
    
    @Test
    public void calculate_OAndA_returnsOAndA() throws Exception {
        Set<BloodType> expected = new HashSet<BloodType>();
        expected.add(BloodType.O);
        expected.add(BloodType.A);
        Set<BloodType> actual = c.calculate(BloodType.O, BloodType.A);
        Assert.assertEquals(expected, actual);
        
        // It's OK when we exchange father's blood type and mother's one.
        actual = c.calculate(BloodType.A, BloodType.O);
        Assert.assertEquals(expected, actual);
    }
    
    private BloodTypeCalculator c;
}
這邊寫法都很固定。首先要讓 JUnit 知道這是 parameterized test,因此前面要加 annotation @RunWith(value = Parameterized.class).

接著寫 public static Collection<Object[]> parameters() method,前面一定要加 @Parameters annotation,此 method 一定要 static,return value type 一定要 Collection<Object[]>, Collection<Object[][]> 等等之類的,Object 的 dimension 就是 number of parameters of ctor. 至於 method name,自己喜歡就好了。

parameter() method 內容請直接模仿上面寫法,保證不會錯。

當 JUnit 準備執行此 test class,她會先看 parameter() method,讀到 2 筆參數,就為這個 test class 生成 2 個 test class objects. 接著再執行內部的 test methods.

因為 OpenRulesBloodTypeCalculator class 沒有 real implementation,所以全部 failure.


PS:
  • JUnit 中,error 和 failure 代表不同的意義
  • Test method 的 naming rule: 分三段命名。
    • Method name: The name of the method you are testing.
    • State under test: The conditions used to produce the expected behavior.
    • Expected behavior: What you expect the tested method to do under the specified conditions.
    參考書籍:Roy Osherove, The Art of Unit Testing: With Examples in .Net

沒有留言:

張貼留言