Automated Testing
Learning Objectives
What you’ll learn
How automated unit tests are developed, including the primary libraries for that. Why automated testing is important to the maintainability of a project and how it can be used in automated builds.
Also because the JUnit framework relies heavily on annotations we will look at what they are and how they are used.
Why start here?
Any software project of a reasonable size quickly gets past the point where manual testing of the functionality with each change is practical. Automated testing gives us reassurance that a project can be enhanced or refactored whilst avoiding regressions.
Adding well constructed tests for a method, showing the expected results from specific inputs can help clarify the intent of a method and also lead to confirmation that the method parameters are sensible (e.g. in name, type and order) for its goal.
Self Study
Reading Materials
- Annotations - geeks for geeks annotations in java, dev.java
- Don’t worry about how to create annotations
- The official JUnit 5 user guide, which describes how to write tests, how test lifecycle works, what annotations are available, etc. JUnit 5 user guide
- Important Sections: 1.4, 2.1-2.7, 4.1, 4.2
- Articles summarising JUnit 5 basics: @Test, assertions, lifecycle, visibility rules; Junit 5 basics, baeldung junit-5, vogella JUnit
- Maven surefire plugin (reference for how tests are run during a build) documentation
- Simple project example example
Video Materials
- Beginner-friendly tutorials on creating and running your first JUnit 5 tests
- Creating and running a test (IntelliJ)
- JUnit 5 Crash Course (first ~25mins)
- More advanced use of JUnit (up to ~30mins when it starts on Mockito) - Mastering Java Testing
Exercises
✍️Exercise 1.1 - Calculator test
Given this class and fill out some sensible tests in the skeleton
Code block 1 Calculator.java
package com.example;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a\ * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}Code block 2 TestCalculator.java
package com.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.\*;
class CalculatorTest {
@BeforeEach
void setUp() {}
@Test
void testAdd() {}
@Test
void testDivide() {}
@Test
void testDivideByZeroThrows() {}
}✍️Exercise 1.2 - Extend Calculator
Write some tests for these methods to be added to the calculator class;
int square(int a)double squareRoot(int a)
Consider:
- what should happen if
squareRootis requested for a negative number? - how would we check the answer for
squareRoot(3)? (think accuracy required)
✍️Exercise 1.3 - String Utilities
Write some tests for this class and then complete the implementation.
Code block 3 StringUtilities
public class StringUtilities {
public boolean isPalindrome(String s) { ... }
public List<String> splitCommaSeparatedLine(String s) { ... }
}Did writing the test cases upfront help you focus on the behaviour you needed to implement? Did you manage to think of edge cases that should be covered? Conversely, what can be a downside (e.g. what tends to happen) if you write tests for a pre-existing implementation?
Stretch Goal - parameterised tests
Can you write a test-case for the isPalindrome method that uses @ParameterizedTest and @MethodSource or @CsvSource to provide parameters to the test? Can you change so that there is a clearer description associated with each test based on the inputs and expectations?
✍️Exercise 1.4 - Maven Project
Create a Maven Project with JUnit 5 dependencies
Add the Calculator and StringUtilities classes as well as their tests. Note that source and test classes should be in separate folders (src/main/java, src/test/java). What do you notice if they are all in src/main/java?
Build the project (mvn clean test) and see that the tests run. Amend one of the implementations so that a test will fail and re-build the project.
Questions
When this class test is run, what is the output?
Code block 4 StringUtilities
public class ExampleTest {
@BeforeEach
public void setup() {
System.out.println("setting up test data");
...
}
@Test
public void testExample1() {
System.out.println("running test case 1");
...
}
@AfterAll
public void closeConnections() {
System.out.println("closing connections");
...
}
@BeforeAll
public void createConnections() {
System.out.println("creating connections");
...
}
}A) setting up test data, creating connections, running test case 1, closing connections B) setting up test data, running test case 1, closing connections, creating connections C) creating connections, setting up test data, running test case 1, closing connections D) creating connections, setting up test data, closing connections, running test case 1
If we introduce a new @Test annotated method testExample2 and run the test class how many times do we expect the following messages to appear;
- setting up test data
- running test case 1
- closing connections
- creating connections
Reflection on key concepts in automated testing
- Explain why tests should be independent of each other. What benefit does this bring?
- Explain the difference between a test-case, a test class and a test suite. Why might we use test suites?
- Explain the Junit test lifecycle
- particularly how we can use this to re-use state or reset state between tests and why this is useful from a maintainability and performance perspective.
- Are there situations where a test case might not have any assertions?
- What might a test without assertions miss and how could it be improved?
JavaDoc
Learning Objectives
What you’ll learn
How to write useful javadoc, how it is structured and why it is important.
Why start here?
Most classes and methods are used by people other than the original developer. Providing a concise description of what a class / method is for and how to use it ensures that the correct expectations are set and that the developer is not surprised by any results/behaviours.
Self Study
Reading Materials
- Wikipedia - Javadoc
- Oracle tool and specification - overview javadoc spec
- example style guide - javadoc-tool
- Examples;
- Java Util
- Apache Commons Lang
Video Materials
Key Concepts
- Good javadoc is more than comments on the implementation
- Javadoc should depend on context;
- frameworks should illustrate how code is written to run using them
- libraries should illustrate how they are to be used
- Links can be to other parts of the code or to external materials e.g. for more complete explanations.
- Javadoc on tests is as important as on the source;
- useful javadoc will help provide context around edge cases and unusual assertions of behaviour
Exercises
In these exercises focus on;
- usage of the core syntax elements; @param, @return, @throws, @see, @link
- what a user of the service needs to know
- example usage, particularly any edge cases
- what tests are showing you about specific methods and their behaviours, in particular any edge cases that may need more explanation.
✍️Exercise 2.1 - Calculator
Given the Calculator class from exercise 1.1 and 1.2 write javadoc for the source classes and methods and for the test class and test methods.
Are there any cases where @see might be useful? think about how certain methods might relate to each other, e.g. inverse operations.
✍️Exercise 2.2 - StringUtilities
StringUtilities class from exercise 1.3, write javadoc for the source classes and methods and for the test class and test methods.