Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách tạo và thực hiện các test case với tham số hóa trong Junit (parameterized test).
Nội dung
JUnit Parameterized Test là gì?
Một parameterized test là một phương thức test bình thường, được thực hiện lặp đi lặp lại bằng cách sử dụng các tham số test khác nhau. Nó giúp chúng ta tiết kiệm thời gian trong việc thực hiện nhiều lần cùng một phương thức test với các loại đầu vào khác nhau để kiểm tra các kết quả khác nhau của chức năng.
Chúng ta có thể truyền đối số vào các phương thức unit test thông qua các cách sau:
- Truyền thông qua đối số của constructor (Constructor Injection).
- Đánh dấu annotation @Parameter trên các filed (Fields injection).
Ví dụ sử dụng Parameterized Test
Giả sử chúng ta có một lớp cần test như sau:
package com.gpcoder.junit.parameterized; public class Fibonacci { public static int compute(int n) { if (n <= 1) { return n; } return compute(n - 1) + compute(n - 2); } }
Sử dụng Parameterized thông qua Constructor
Chúng ta sẽ thực hiện lần lượt các bước sau:
- Tạo một class test được đánh dấu @RunWith(Parameterized.class).
- Tạo một public static method được đánh dấu @Parameters và return một Collection của Objects (ví dụ Array) như bộ dữ liệu test.
- Tạo một public constructor nhận các đối số bằng với số phần tử của một object trong bộ dữ liệu test.
- Tạo các test case sử dụng các field đã được khởi tạo trong constructor.
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertEquals; import java.util.Arrays; import java.util.Collection; 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 ParameterizedContructorTest { private int number; private int expected; // Inject via constructor public ParameterizedContructorTest(int number, int expected) { this.number = number; this.expected = expected; } // The name attribute is optional, provide an unique name for test @Parameters(name = "{index}: Fibonacci({0}) = {1}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); } @Test public void test_addTwoNumbes() { assertEquals(expected, Fibonacci.compute(number)); } }
Kết quả test của chương trình trên:
Sử dụng Parameterized thông qua Fields
Chúng ta sẽ thực hiện lần lượt các bước sau:
- Tạo một class test được đánh dấu @RunWith(Parameterized.class).
- Tạo một public static method được đánh dấu @Parameters và return một Collection của Objects (ví dụ Array) như bộ dữ liệu test.
- Tạo một public field được đánh dấu @Parameter(value = index).
- Tạo các test case sử dụng các field đã được khai báo với @Parameter.
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertEquals; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(value = Parameterized.class) public class ParameterizedMultiFieldTest { // The index of the parameter is 0 // Default value is always "value=0" @Parameter(value = 0) public int number; // NOT private // The index of the parameter is 1 @Parameter(value = 1) public int expected; // NOT private // The name attribute is optional, provide an unique name for test @Parameters(name = "{index}: Fibonacci({0}) = {1}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); } @Test public void testFibonacci() { assertEquals(expected, Fibonacci.compute(number)); } }
Chạy test class trên, chúng ta có cùng kết quả với việc sử dụng Parameterized thông qua Constructor.
Trường hợp phương thức test chỉ có 1 field cần inject, chúng ta không cần thuộc tính value trong @Parameter, mặc định value của nó là 0. Ví dụ:
package com.gpcoder.junit.parameterized; import java.util.Arrays; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(value = Parameterized.class) public class ParameterizedSingleFieldTest { // Default value is always "value=0" @Parameter public int number; // NOT private // The name attribute is optional, provide an unique name for test @Parameters public static Iterable<? extends Object> dataset() { return Arrays.asList(1, 2); } @Test public void testIsPositiveNumber() { Assert.assertEquals(true, number > 0); } }
Kết quả test chương trình trên:
@Theory và @DataPoints là gì?
Ngoài cách sử dụng parameterized test case với @Parameters ở trên, chúng ta có thể sử dụng Annotation @Theory và @DataPoints.
Tương tự như @Parameters, @DataPoints được sử dụng để return một bộ dữ liệu test. Tuy nhiên, nó đơn giản hơn @Parameters, chúng ta không cần phải tạo một constructor hay field tương ứng.
Để thực thi test với các @DataPoints, chúng ta cần phải sử dụng @RunWith(Theories.class). Các phương thức test sử dụng data từ @DataPoints, chúng ta sẽ thay thế @Test trên phương thức bằng @Theory.
Ví dụ 1: chúng ta sẽ sử dụng lại các ví dụ ở trên, với cách dùng @Theory và @DataPoints.
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertEquals; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class TheoryAndDataPointTest1 { @DataPoints public static int[][] data() { return new int[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }; } @Theory public void testFibonacci(final int[] inputs) { System.out.println(String.format("Testing with %d and %d", inputs[0], inputs[1])); assertEquals(inputs[1], Fibonacci.compute(inputs[0])); } }
Ví dụ 2: Trường hợp chúng ta muốn tạo từng bộ data riêng riêng lẻ.
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertEquals; import org.junit.experimental.theories.DataPoint; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class TheoryAndDataPointTest2 { @DataPoint public static int[] input1 = new int[] { 0, 0 }; @DataPoint public static int[] input2 = new int[] { 1, 1 }; @DataPoint public static int[] input3 = new int[] { 2, 1 }; @DataPoint public static int[] input4 = new int[] { 3, 2 }; @DataPoint public static int[] input5 = new int[] { 4, 3 }; @DataPoint public static int[] input6 = new int[] { 5, 5 }; @DataPoint public static int[] input7 = new int[] { 6, 8 }; @Theory public void testFibonacci(final int[] inputs) { System.out.println(String.format("Testing with %d and %d", inputs[0], inputs[1])); assertEquals(inputs[1], Fibonacci.compute(inputs[0])); } }
Ví dụ 3: Trường hợp có nhiều @DataPoints trong cùng một class test, chúng ta có thể đặt tên cho chúng, và sử dụng @FromDataPoints.
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.FromDataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class TheoryAndDataPointTest3 { @DataPoints("data1") public static int[][] dataPoints = new int[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }; @DataPoints("data2") public static int[][] data() { return new int[][] { { 0, 999 }}; } @Theory public void givenNumber_WhenValidValue_ThenEquals(@FromDataPoints("data1") final int[] inputs) { System.out.println(String.format("Testing with %d and %d", inputs[0], inputs[1])); assertEquals(inputs[1], Fibonacci.compute(inputs[0])); } @Theory public void givenNumber_WhenInvalidValue_ThenNotEquals(@FromDataPoints("data2") final int[] inputs) { System.out.println(String.format("Testing with %d and %d", inputs[0], inputs[1])); assertNotEquals(inputs[1], Fibonacci.compute(inputs[0])); } }
Ví dụ 4: Trường hợp chúng ta muốn test một bộ dữ liệu 1 với từng dữ liệu trong bộ dữ liệu 2, chúng ta có thể sử dụng @TestedOn
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertTrue; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.experimental.theories.suppliers.TestedOn; import org.junit.runner.RunWith; class Checker { public static boolean isGreaterThan(int number1, int number2) { return number1 > number2; } } @RunWith(Theories.class) public class TheoryAndDataPointTest4 { @Theory public void testAllNumber1GreaterThanAllNumber2(@TestedOn(ints = { 3, 4, 5 }) int number1, @TestedOn(ints = { 1, 2 }) int number2) { System.out.println(String.format("Testing with %d and %d", number1, number2)); assertTrue(Checker.isGreaterThan(number1, number2)); } }
Output của test trên:
Testing with 3 and 1 Testing with 3 and 2 Testing with 4 and 1 Testing with 4 and 2 Testing with 5 and 1 Testing with 5 and 2
Ví dụ 5: Chúng ta sẽ tạo một custom Parameter Supplier tương tự như @TestedOn
package com.gpcoder.junit.parameterized; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.junit.experimental.theories.ParametersSuppliedBy; @Retention(RetentionPolicy.RUNTIME) @ParametersSuppliedBy(BetweenSupplier.class) public @interface Between { int first(); int last(); }
package com.gpcoder.junit.parameterized; import java.util.ArrayList; import java.util.List; import org.junit.experimental.theories.ParameterSignature; import org.junit.experimental.theories.ParameterSupplier; import org.junit.experimental.theories.PotentialAssignment; public class BetweenSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable { Between annotation = sig.getAnnotation(Between.class); List<PotentialAssignment> list = new ArrayList<>(); for (int i = annotation.first(); i <= annotation.last(); i++) { list.add(PotentialAssignment.forValue(null, i)); } return list; } }
package com.gpcoder.junit.parameterized; import static org.junit.Assert.assertTrue; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; class Checker2 { public static boolean isGreaterThan(int number1, int number2) { return number1 > number2; } } @RunWith(Theories.class) public class TheoryAndDataPointTest5 { @Theory public void multiplyIsInverseOfDivideWithInlineDataPoints(@Between(first = 3, last = 5) int number1, @Between(first = 1, last = 2) int number2) { System.out.println(String.format("Testing with %d and %d", number1, number2)); assertTrue(Checker2.isGreaterThan(number1, number2)); } }
Thực thi class trên, chúng ta có cùng kết quả như @TestedOn.
Tài liệu tham khảo: