Simplify Compliance Workflows With New C/C++test 2024.2 & AI-Driven Automation | Register Now
Jump to Section
How to Create JUnit Parameterized Tests
Writing parameterized test code can be a lot of work because each test needs a bulk of correctly written code. Fortunately, here is how you can use Parasoft Jtest to automatically generate your parameterized tests without writing boilerplate codes.
Jump to Section
Jump to Section
Parameterized tests are a good way to define and run multiple test cases, where the only difference between them is the data. Here, we look at three different frameworks commonly used with JUnit tests.
When writing unit tests, it is common to initialize method input parameters and expected results in the test method itself. In some cases, using a small set of inputs is enough; however, there are cases in which we need to use a large set of values to verify all of the functionality in our code. Parameterized tests are a good way to define and run multiple test cases, where the only difference between them is the data. They can validate code behavior for a variety of values, including border cases. Parameterizing tests can increase code coverage and provide confidence that the code is working as expected.
There are a number of good parameterization frameworks for Java. In this article, we will look at three different frameworks commonly used with JUnit tests, with a comparison between them and examples of how the tests are structured for each. Finally, we will explore how to simplify and expedite the creation of parameterized tests.
JUnit Parameterized Test Frameworks
Let’s compare the 3 most common frameworks: JUnit 4, JunitParams, and JUnit 5. Each JUnit parameterization framework has its own strengths and weaknesses.
JUnit 4
Pros:
- This is the parameterization framework built into JUnit 4, so it requires no additional external dependencies.
- It supports older versions of Java (JDK 7 and older).
Cons:
- Test classes use fields and constructors to define parameters, which make tests more verbose.
- It requires a separate test class for each method being tested.
JunitParams
Pros:
- Simplifies parameter syntax by allowing parameters to be passed directly to a test method.
- Allows multiple test methods (each with their own data) per test class.
- Supports CSV data sources, as well as annotation-based values (no method required).
Cons:
- Requires the project to be configured with the JunitParams dependency.
- When running and debugging tests, all tests within the class must be run – it is not possible to run a single test method within a test class.
JUnit 5
Pros:
- This parameterization framework is built into JUnit 5 and improves what was included with JUnit 4.
- Has a simplified parameter syntax like JunitParams.
- Supports multiple data-set source types, including CSV and annotation (no method required).
- Even though no extra dependencies are required, more than one .jar is needed.
Cons:
- Requires Java 8 and newer version of build system (Gradle 4.6 or Maven Surefire 2.21).
- May not be supported in your IDE yet (as of this writing, only Eclipse and IntelliJ support JUnit 5).
Example
As an example, suppose that we have a method that processes loan requests for a bank. We might write a unit test that specifies the loan request amount, down payment amount, and other values. We would then create assertions that validate the response – the loan may be approved or rejected, and the response may specify the terms of the loan.
For example:
public LoanResponse requestLoan(float loanAmount, float downPayment, float availableFunds) { LoanResponse response = new LoanResponse(); response.setApproved(true); if (availableFunds < downPayment) { response.setApproved(false); response.setMessage("error.insufficient.funds.for.down.payment"); return response; } if (downPayment / loanAmount < 0.1) { response.setApproved(false); response.setMessage("error.insufficient.down.payment"); } return response; }
parameterized_test_example_1.java
@Test public void testRequestLoan() throws Throwable { // Given LoanProcessor underTest = new LoanProcessor(); // When LoanResponse result = underTest.requestLoan(1000f, 200f, 250f); // Then assertNotNull(result); assertTrue(result.isApproved()); assertNull(result.getMessage()); }
In order to make sure that our requestLoan() method is tested thoroughly, we need to test with a variety of down payments, requested loan amounts, and available funds. For instance, let’s test a $1 million loan request with zero down payment, which should be rejected. We could simply duplicate the existing test with different values, but since the test logic would be the same, it is more efficient to parameterize the test instead.
We will parameterize the requested loan amount, down payment, and available funds, as well as the expected results: whether the loan was approved, and the message returned after validation. Each set of request data, along with its expected results, will become its own test case.
An Example Parameterized Test Using JUnit 4 Parameterized
Let’s start with a Junit 4 Parameterized example. To create a parameterized test, we first need to define the variables for the test. We also need to include a constructor to initialize them:
@RunWith(Parameterized.class) public class LoanProcessorParameterizedTest { float loanAmount; float downPayment; float availableFunds; boolean expectApproved; String expectedMessage; public LoanProcessorParameterizedTest(float loanAmount, float downPayment, float availableFunds, boolean expectApproved, String expectedMessage) { this.loanAmount = loanAmount; this.downPayment = downPayment; this.availableFunds = availableFunds; this.expectApproved = expectApproved; this.expectedMessage = expectedMessage; } // ... }
parameterized_test_example_3.java
Hosted by GitHub
Here, we see that the test uses the @RunWith annotation to specify that the test will run with the Junit4 Parameterized runner. This runner knows to look for a method that will provide the value-set for the test (annotated with @Parameters), initialize the test properly, and run the tests with multiple rows.
Note that each parameter is defined as a field in the test class, and the constructor initializes these values (you can also inject values into fields using the @Parameter annotation if you don’t want to create a constructor). For each row in the value-set, the Parameterized runner will instantiate the test class and run each test in the class.
Let’s add a method that provides the parameters to the Parameterized runner:
@Parameters(name = "Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}") public static Iterable<Object[]> data() throws Throwable { return Arrays.asList(new Object[][] { { 1000.0f, 200.0f, 250.0f, true, null } }); }
parameterized_test_example_4.java
Hosted by GitHub
The value-sets are built as a List of Object arrays by the data() method, which is annotated with @Parameters. Note that @Parameters sets the name of the test using placeholders, which will be replaced when the test runs. This makes it easier to see values in test results, as we will see later. Currently, there is only one row of data, testing a case where the loan should be approved. We can add more rows to increase coverage of the method under test.
@Parameters(name = "Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}") public static Iterable<Object[]> data() throws Throwable { return Arrays.asList(new Object[][] { { 1000.0f, 200.0f, 250.0f, true, null }, { 1000.0f, 50.0f, 250.0f, false, "error.insufficient.down.payment" }, { 1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment" } }); }
parameterized_test_example_5.java
Hosted by GitHub
Here, we have one test case where the loan would be approved, and two cases in which it should not be approved for different reasons. We may want to add rows in which zero or negative values are used, as well as test boundary conditions.
We are now ready to create the test method:
@Test public void testRequestLoan() throws Throwable { // Given LoanProcessor underTest = new LoanProcessor(); // When LoanResponse result = underTest.requestLoan(loanAmount, downPayment, availableFunds); // Then assertNotNull(result); assertEquals(expectApproved, result.isApproved()); assertEquals(expectedMessage, result.getMessage()); }
parameterized_test_example_6.java
Hosted by GitHub
Here, we reference the fields when invoking the requestLoan() method and validating the results.
JunitParams Example
The JunitParams library simplifies parameterized test syntax by allowing parameters to be passed directly to the test method. The parameter values are provided by a separate method whose name is referenced in the @Parameters annotation.
@RunWith(JUnitParamsRunner.class) public class LoanProcessorParameterizedTest2 { @Test @Parameters(method = "testRequestLoan_Parameters") public void testRequestLoan(float loanAmount, float downPayment, float availableFunds, boolean expectApproved, String expectedMessage) throws Throwable { ... } @SuppressWarnings("unused") private static Object[][] testRequestLoan_Parameters() throws Throwable { // Parameters: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4} return new Object[][] { { 1000.0f, 200.0f, 250.0f, true, null }, { 1000.0f, 50.0f, 250.0f, false, "error.insufficient.down.payment"}, { 1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment" } }; } }
parameterized_test_example_7.java
Hosted by GitHub
JunitParams has the additional benefit that it supports using CSV files to provide values in addition to providing the values in code. This allows the test to be decoupled from the data and data values to be updated without updating the code.
Junit 5 Example
JUnit 5 addresses some of the limitations and shortcomings of JUnit 4. Like JunitParams, Junit 5 also simplifies the syntax of parameterized tests. The most important changes in syntax are:
- The test method is annotated with @ParameterizedTest instead of @Test
- The test method accepts parameters directly, instead of using fields and a constructor
- The @RunWith annotation is no longer needed
Defining the same example in Junit 5 would look like this:
public class LoanProcessorParameterizedTest { @ParameterizedTest(name="Run {index}: loanAmount={0}, downPayment={1}, availableFunds={2}, expectApproved={3}, expectedMessage={4}") @MethodSource("testRequestLoan_Parameters") public void testRequestLoan(float loanAmount, float downPayment, float availableFunds, boolean expectApproved, String expectedMessage) throws Throwable { ... } static Stream<Arguments> testRequestLoan_Parameters() throws Throwable { return Stream.of( Arguments.of(1000.0f, 200.0f, 250.0f, true, null), Arguments.of(1000.0f, 50.0f, 250.0f, false, "error.insufficient.down.payment"), Arguments.of(1000.0f, 200.0f, 150.0f, false, "error.insufficient.funds.for.down.payment") ); } }
parameterized_test_example_8.java
Hosted by GitHub
Efficiently Create Parameterized Tests
As one might imagine, writing the above-parameterized test can be a bit of work. For each parameterized test framework there is a bit of boilerplate code that needs to be written correctly. It can be hard to remember the correct structure, and it takes time to write out. To make this much easier, you can use Parasoft Jtest to generate parameterized tests, automatically, like the ones described above. To do this, simply select the method you want to generate a test for (in Eclipse or IntelliJ),:
The test is generated, using default values and assertions. You can then configure the test with real input values and assertions, and add more data rows to the data() method.
Running the Parameterized Test
Parasoft Jtest can run parameterized tests directly in both Eclipse and IntelliJ.
The JUnit View in Eclipse
Note that the name of each test, as shown, includes input values from the dataset and expected result values. This can make debugging the test much easier when it fails, since the input parameters and expected outputs are shown for each case.
You can also use the Run All action from Parasoft Jtest:
The Flow Tree view in Parasoft Jtest
It analyzes the test flow and provides detailed information about the previous test run. This allows you to see what happened in the test without needing to rerun the test with breakpoints or debugging statements. For instance, you can see parameterized values in the Variables view:
The Variables View in Parasoft Jtest
Conclusion
Each of the three frameworks that we reviewed is a fine choice and works well. If using JUnit 4, I prefer JunitParams over the built-in JUnit 4 Parameterized framework, due to the cleaner design of the test classes and the ability to define multiple test methods in the same class. However, if using JUnit 5, I’d recommend the built-in JUnit 5 framework since it addresses the shortcomings in JUnit 4 and requires no extra libraries. I also like using Parasoft’s unit testing capabilities to make the creation, execution, and debugging of parameterized tests more efficient.
Learn how Parasoft Jtest can help you improve your Java code quality and team productivity.