Regardless of JUnit, the fixture setup code in the test code is It tends to be long and reduces readability.
In this article, we focus on "input value" and "expected value" from the elements of the test fixture. We will summarize the method of clearly describing those preparations by parameterization.
The four major steps required for JUnit parameterization testing are:
If you want to do a parameterized test, put the RunWith annotation of the test class Specify the org.junit.experimental.theories.Theories class. The Teories test runner can be used with the Enclosed test runner.
@RunWith(Enclosed.class)
public class ParameterizedTest {
@RunWith(Theories.class)
public static class For numbers{
}
@RunWith(Theories.class)
public static class For strings{
}
@RunWith(Theories.class)
public static class For numbers and strings{
}
@RunWith(Theories.class)
public static class For numbers and numbers{
}
}
For test methods that want to use parameters, Add Theory annotation instead of Test annotation.
Test methods with Theory annotation can declare arbitrary arguments. Members with the DataPoint and DataPoints annotations described below Set in the argument as a parameter.
@RunWith(Enclosed.class)
public class ParameterizedTest {
@RunWith(Theories.class)
public static class For numbers{
@Theory
public void testCase(int num) throws Exception {
}
}
@RunWith(Theories.class)
public static class For strings{
@Theory
public void testCase(String str) throws Exception {
}
}
@RunWith(Theories.class)
public static class For numbers and strings{
@Theory
public void testCase(int num, String str) throws Exception {
}
}
@RunWith(Theories.class)
public static class For numbers and numbers{
@Theory
public void testCase(int num1, int num2) throws Exception {
}
}
}
In the parameterized test using Theories test runner, Define parameters using DataPoint annotations. Parameters are defined in static and public fields or methods.
The argument of the method with Theory annotation is Of the values annotated with DataPoint, the parameters that match the type All combinations are passed as arguments.
@RunWith(Enclosed.class)
public class ParameterizedTest {
@RunWith(Theories.class)
public static class For numbers{
@DataPoint
public static int INT_PARAM_1 = 3;
@DataPoint
public static int INT_PARAM_2 = 4;
@Theory
public void testCase(int num) throws Exception {
System.out.println("Input value:" + num);
}
}
@RunWith(Theories.class)
public static class For strings{
@DataPoint
public static String STR_PARAM_1 = "Hello";
@DataPoint
public static String STR_PARAM_2 = "World";
@Theory
public void testCase(String str) throws Exception {
System.out.println("Input value:" + str);
}
}
@RunWith(Theories.class)
public static class For numbers and strings{
@DataPoint
public static int INT_PARAM_1 = 3;
@DataPoint
public static int INT_PARAM_2 = 4;
@DataPoint
public static String STR_PARAM_1 = "Hello";
@DataPoint
public static String STR_PARAM_2 = "World";
@Theory
public void testCase(int num, String str) throws Exception {
System.out.println("Input value:" + num + "、" + str);
}
}
@RunWith(Theories.class)
public static class For numbers and numbers{
@DataPoint
public static int INT_PARAM_1 = 3;
@DataPoint
public static int INT_PARAM_2 = 4;
@Theory
public void testCase(int num1, int num2) throws Exception {
System.out.println("Input value:" + num1 + "、" + num2);
}
}
}
When ParameterizedTest is executed, the following contents are output to the console.
~ In the case of numerical value ~
Input value: 3
Input value: 4
~ For character strings ~
Input value: Hello
Input value: World
~ For numbers and strings ~
Input value: 3, Hello
Input value: 3, World
Input value: 4, Hello
Input value: 4, World
~ In the case of numerical values and numerical values ~
Input value: 3, 3
Input values: 3, 4
Input values: 4, 3
Input values: 4, 4
If more arguments are passed to Theory method By declaring a static DTO in the test class, the arguments can be combined into one. A DTO that holds arguments is called a fixture object.
@RunWith(Theories.class)
public class ParameterizedTest {
@DataPoint
public static Fixture INT_PARAM_1 = new Fixture(1, 2, 3);
@DataPoint
public static Fixture INT_PARAM_2 = new Fixture(0, 2, 2);
@Theory
public void testCase(Fixture params) throws Exception {
assertThat(params.x + params.y, is(params.expected));
}
static class Fixture {
int x;
int y;
int expected;
Fixture(int x, int y, int expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
}
}
Only one parameter could be defined in the DataPoint annotation, With DataPoints annotation, multiple parameters can be defined in one place.
@RunWith(Theories.class)
public class ParameterizedTest {
@DataPoints
public static Fixture[] INT_PARAMS = {
new Fixture(1, 2, 3),
new Fixture(0, 2, 2),
};
/*Definition using DataPoint annotation
@DataPoint
public static Fixture INT_PARAM_1 = new Fixture(1, 2, 3);
@DataPoint
public static Fixture INT_PARAM_2 = new Fixture(0, 2, 2);
*/
@Theory
public void testCase(Fixture params) throws Exception {
assertThat(params.x + params.y, is(params.expected));
}
static class Fixture {
int x;
int y;
int expected;
Fixture(int x, int y, int expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
}
}
params.yaml
!!seq [
!!test.ParameterizedTest$Fixture
{ x: 1, y: 2, expected: 3 },
!!test.ParameterizedTest$Fixture
{ x: 0, y: 2, expected: 2 },
]
@RunWith(Theories.class)
public class ParameterizedTest {
@DataPoints
public static Fixture[] INT_PARAMS = {
InputStream in = ParameterizedTest.class
.getResourceAsStream("params.yaml");
return ((List<Fixture>) new Yaml().load(in)).toArray(new Fixture[0]);
};
/*Definition using DataPoint annotation
@DataPoint
public static Fixture INT_PARAM_1 = new Fixture(1, 2, 3);
@DataPoint
public static Fixture INT_PARAM_2 = new Fixture(0, 2, 2);
*/
@Theory
public void testCase(Fixture params) throws Exception {
assertThat(params.x + params.y, is(params.expected));
}
static class Fixture {
int x;
int y;
int expected;
Fixture(int x, int y, int expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
}
}
If you just specify the parameters, all combinations of tests will be performed. In such a case, some combinations may not match the expected values of the test.
For example, the above test verified whether the added value is the same as the expected value, If this is a test to verify that the added value is even In the case of x = 1 and y = 2, the input value is not as expected.
In these cases, use the Assume class Only the input values expected by the test case can be passed.
The methods provided by the Assume class include asumeTrue and assumeThat. Implement each as follows.
assumeTrue(Conditional expression);
assumeThat(Input value,matcher method);
As for the movement, when the judgment in the method is false in both cases, In other words, if the input value is not as expected, an AssumptionViolatedException will be generated. No further processing is performed. (Acts as a return clause in the test code) This exception is treated specially within the test runner, and catching the exception will result in a successful test result.
@RunWith(Theories.class)
public class ParameterizedTest {
@DataPoints
public static Fixture[] INT_PARAMS = {
new Fixture(1, 2, 3),
new Fixture(0, 2, 2),
};
@Theory
public void testCase(Fixture params) throws Exception {
//The input value is assumed to be an even number after addition.
//Unexpected input value(assumeTrue(false)What becomes)In the case of, the subsequent processing is not performed
assumeTrue((params.x + params.y) % 2 == 0);
// x + y =Verify that it is even
assertThat((params.x + params.y) % 2 == 0 , is(true));
}
static class Fixture {
int x;
int y;
int expected;
Fixture(int x, int y, int expected) {
this.x = x;
this.y = y;
this.expected = expected;
}
}
}
Comprehensiveness, which is one index of test quality, depends on parameter specification and Assume class. Since it is the implementer who does the filtering, just because all combinations are tested It is not a test case that covers requirements and specifications. Therefore, regarding the validity and completeness of test data, based on a reviewer who is familiar with test techniques, It is necessary to take measures such as verification.
When testing with Theories test runner, the report displayed when the test fails The information "Which parameter failed?" Is missing. Therefore, in the parameterized test, it is necessary to take measures in consideration of the ease of investigation when the test fails.
/*Example) Use the assertThat method to output the input value to the message at the time of failure*/
@Theory
public void testCase(Fixture params) throws Exception {
assumeTrue((params.x + params.y) % 2 == 0);
String failMsg = "Fail when x = " + params.x + ", y = " + params.y;
assertThat(failMsg, (params.x + params.y) % 2 == 0 , is(true));
}
This article was written with reference to the following information.
-[Introduction to JUnit Practice ── Systematic Unit Testing Techniques](https://www.amazon.co.jp/JUnit%E5%AE%9F%E8%B7%B5%E5%85%A5%E9 % 96% 80-% E4% BD% 93% E7% B3% BB% E7% 9A% 84% E3% 81% AB% E5% AD% A6% E3% 81% B6% E3% 83% A6% E3% 83% 8B% E3% 83% 83% E3% 83% 88% E3% 83% 86% E3% 82% B9% E3% 83% 88% E3% 81% AE% E6% 8A% 80% E6% B3% 95-WEB-PRESS-plus-ebook / dp / B07JL78S95)
Recommended Posts