Test Automation with JUnit 5
Test Automation with JUnit 5
Test Automation
To use Software to design, generate, execute, and determine the outcome (i.e., compare actual vs. expected output) of test cases and/or control such or related activities
Revenue tasks vs. Excise tasks
- Some tasks need human wisdom, intelligence, judgement, domain knowledge etc.
- Some tasks can be automated following a well-defined process/algorithm/standard.
Software Testability
Testability is how testable the software is. It can be gauged by:
- observability: how easy it is to observe the behavior of a software in terms of its outputs and/or effects on the environment.
- controllability: how easy it is to provide a program with the needed inputs, in terms of values, operations, and behaviors.
Test case values are input values necessary to complete the execution of the test case Prefix values: inputs necessary to establish the appropriate state so that the test case can be properly run. Postfix values: inputs to be sent to the software after the test case has run Expected results: the corrected (i.e. expected) output for a given test case.
A test case is composed of test case values, prefix and postfix values, and expected results.
- It can also contain test case ID ,test case summary, test case author, last status (pass/fail), actual result, etc.
- At the very least a test case must include the test caste value and expected results.
Test Set (aka Test Suite): a set of test cases Test Framework (aka Test Tool): software tool that facilitates testing activities
JUnit
JUnit is the most popular/well-known test framework created by Kent Beck and Eric Gamma.
- Used for unit testing and integration testing.
- Can be launched from the command-line or IDEs.
- xUnit Frameworks include: CUnit, CppUnit, JSUnit, PyUnit, NUnit, EUnit, RUnit, and more.
Use one or more test classes where each test class contains:
- test cases written as test methods
- methods to setup program state before each test method is run (i.e., prefix values)
- methods to update state after each test method is run (i.e., postfix values)
A variety of assert methods available to check the actual result produced by a test case against expected output.
For example,
class MathUtilsTest { // test class contains test methods (i.e., test cases)
ArrayList<Integer> ray;
@BeforeEach
public static void setUp() {
ray = new AraryList<Integer>();
}
@AfterEach
public static void tearDown() {
ray.clear();
}
@Test
public void testAdd() { // a test method corresponding to a single test case
MathUtils mu = new MathUtils();
assertEquals(8, mu.add(2, 6));
}
}
Test Instance Lifecycle: In order to allow individual test methods to be executed in isolation and to avoid unexpected side effects due to mutable test instance states, JUnit creates a new instance of each test class before executing each test method.
- Override this by
@TestInstance(Lifecycle.PER_CLASS)
Exceptions
In order to make sure that an error is thrown there are two ways:
@Test
public void testCountOfRayNull() {
ray = null;
try {
ArrayUtils.countOf(ray, 2); }
catch (NullPointerException e) {
return;
}
fail(“null must fail”);
}
@Test
public void testCountOfRayNull() {
ray = null;
assertThrows(NullPointerException.class, () -> {
ArrayUtils.countOf(ray, 2);
});
} //assertThrows is only supported in JUnit 5
Parameterized Tests
Parameterize Testing is a common unit testing pattern used to test a single method with different input vlaues.
- One way is to write a separate test method for each test case, but this creates lots of duplicated code
Example of the "add" method:
@DisplayName("MathUtils.add Tests")
@ParameterizedTest
@CsvSource({"0, 1, 1", "2, -4, -2", "-3, 3, 0", “-2, -2, -4"})
public void testAdd(int a, int b, int expected) {
assertEquals(expected, MathUtils.add(a, b));
}
Example of the "countOf" method for few inputs that we expect to return the value of 1:
@ParameterizedTest
@MethodSource("createListAndKey")
public void testCountOfExpectOne(ArrayList<Integer> ray, int key) {
assertEquals(1, ArrayUtils.countOf(ray, key));
}
private static Stream<Arguments> createListAndKey() {
rayList<Integer> ray0 = new ArrayList<Integer>(); ray0.add(2);
ray0.add(5);
ray0.add(-1);
return Stream.of(
Arguments.of(ray0, 2),
Arguments.of(ray0, 5),
Arguments.of(ray0, -1)
);
}
@Nested
Use @Nested to have nested test classes. This is useful to group test cases.
@DisplayName(“Tree Test")
class TreeTest {
// ...
@Nested
@DisplayName(“Add new node")
class AddNode {
// ...
}
// ...
@Nested
@DisplayName(“Remove a node")
class RemoveNode {
// ...
}
// ...
}
Tags and Filtering
Tags are used to filter which tests are executed for a given test plan. For example, a development team may tag tests with values such as "fast", "slow", etc. and then supply a list of tags to be used for the current test plan
- Can give test methods and/or test classes one or more tag names
@Tag(“all")
class ClassATest {
@Test
@Tag("development") @Tag("production")
void testCaseA(TestInfo testInfo) { }
}
Repeated Test
To run a test more than once,
@RepeatedTest(10)
public void testRepeat() {
// ...
}
Order of Execution
By default in JUnit, the test methods could run in any order
- When unit testing, test cases must be independent of each other
Possible test case execution orderings:
- Alphanumeric: sorts test methods alphanumerically based on their names and formal parameter lists.
- OrderAnnotation: sorts test methods numerically based on values specified via the
@Orderannotation. - Random: orders test methods pseudo-randomly and supports configuration of a custom seed.
OrderAnnotation example:
@TestMethodOrder(OrderAnnotation.class) class OrderOfExecutionDemo {
@Test
@Order(1)
void nullValues() {
// perform assertions against null values
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
}
}
Alphanumeric order example:
@TestMethodOrder(Alphanumeric.class) class OrderOfExecutionDemo {
@Test
void testA() {
// run this first
}
@Test
void testB() {
// run this next
}
@Test
void testC() {
// and run this last
}
}
Timeout
@Timeout allows a test to fail if the execution time exceeds the specified limit.
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS) void failsIfExecutionTimeExceeds100Milliseconds() {
// fails if execution time > 100ms
}
}
Assumptions
Used to validate assumptions before/after test execution. This can customize which tests to run given certain scenarios.
@Test
public void assumeThatFileSeparatorTest(){
// only execute the test if OS is Windows
assumeTrue(File.separatorChar == '\\'); // TEST
}
Disable
@Disable is used to disable a test case:
- useful to ignore test cases
class DisabledTestsDemo {
@Disabled("Disabled until fault #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@Disabled("Disabled until feature #23 is implemented")
@Test
void testWillBeExecutedLater() {
}
// ...
}
Assert
Very common to use Assert libraries in tandem with Junit:
- AssertJ https://joel-costigliola.github.io/assertj/
- Hamcrest https://hamcrest.org/
- Truth https://truth.dev/
Linked from Software Testing and Debugging