[JAVA] Getting Started with Parameterization Testing in JUnit

Overview

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.

Parameterization of input and expected values

The four major steps required for JUnit parameterization testing are:

  1. Give the test class Theories test runner
  2. Annotate the test case with Theory and specify the parameters to receive. (If you do not do step 4, all combinations of specified parameters will be executed)
  3. Prepare the parameters
  4. Define a combination of parameters that will not be executed

1. 1. Theories test runner

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{
    }
}

2. Theory annotation

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 {
        }
    }
}

3. 3. Parameter preparation

DataPoint annotation

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

Use of fixture objects

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;
        }
    }
}

DataPoints annotation

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;
        }
    }
}

External resource (YAML file)

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;
        }
    }
}

Four. Assume class

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;
        }
    }
}

Parameterization test problem

Data completeness

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.

Lack of information about parameters

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));
}

References

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

Getting Started with Parameterization Testing in JUnit
Getting Started with DBUnit
Getting Started with Ruby
Getting Started with Swift
Getting Started with Docker
Getting Started with Doma-Transactions
Getting Started with Doma-Annotation Processing
Getting Started with Java Collection
Getting Started with JSP & Servlet
Getting Started with Java Basics
Getting Started with Spring Boot
Getting Started with Ruby Modules
Testing request sending and receiving logic with MockWebServer in JUnit
Getting Started with Java_Chapter 5_Practice Exercises 5_4
[Google Cloud] Getting Started with Docker
Getting started with Java lambda expressions
Getting Started with Docker with VS Code
Getting Started with Doma-Criteria API Cheat Sheet
Getting Started with Ruby for Java Engineers
Getting Started with Java Starting from 0 Part 1
Getting Started with Ratpack (4)-Routing & Static Content
Getting started with the JVM's GC mechanism
Getting Started with Language Server Protocol with LSP4J
Getting Started with Creating Resource Bundles with ListResoueceBundle
Links & memos for getting started with Java (for myself)
Getting Started with Doma-Using Projection with the Criteira API
Getting Started with Doma-Using Subqueries with the Criteria API
Getting Started with Java 1 Putting together similar things
Getting started with Kotlin to send to Java developers
Getting Started with Doma-Using Joins with the Criteira API
Testing with com.google.testing.compile
Refactoring in JUnit
Getting Started with Doma-Introduction to the Criteria API
I tried Getting Started with Gradle on Heroku
Getting started with Java programs using Visual Studio Code
[JUnit 5] Write a validation test with Spring Boot! [Parameterization test]
Getting Started with Legacy Java Engineers (Stream + Lambda Expression)
Testing model with RSpec
Unit test with Junit.
Get started with Gradle
Proceed with Rust official documentation on Docker container (1. Getting started)
Getting started with Java and creating an AsciiDoc editor with JavaFX
Getting Started with Doma-Dynamicly construct WHERE clauses with the Criteria API
Getting Started with Reactive Streams and the JDK 9 Flow API
Things to keep in mind when testing private methods in JUnit
Getting Started with GitHub Container Registry instead of Docker Hub
Sample code for basic mocking and testing with Mockito 3 + JUnit 5