Unit Testing - TheDavSmasher/softwareconstruction GitHub Wiki
🖥️ Slides
📖 Required Reading: JUnit 5 User Guide
- Sections 1 through 1.4.3
- Sections 2 through 2.2
- Section 2.4
Test driven development (TDD) was popularized in the 1990s as part of the extreme programming wave. The idea is that you begin writing software by creating tests that represent the consumer of your software. You then use the tests to drive the development of your code. When the tests pass you know that your code is complete.
TDD has been proven to decrease development time, provide documentation and examples for your code, result in less bugs, and prevent against the introduction of future bugs. Additionally, by writing your tests by focusing on the consumer of your code, you tend to design better interfaces and accurate domain models.
Today, TDD is a common industry practice that you will be expected to use on a daily basis. However, it takes effort to learn how to write tests that are effective and efficient. Making this a standard part of your development process will give you a significant advantage as you progress in your professional career.
The process of test driven development follows a specific process that includes the following steps.
- Add a test - When a new feature is added to the requirements, or a bug is discovered, you first write a test that demonstrates the correct execution of the code. A simple test is usually a function that exists outside of the code that you would release to production. The test function is then executed by some testing framework. When you execute your new test it should fail because you have not implemented the code that satisfies the test.
- Implement the functionality - With the test in place you write the functionality necessary to implement the feature or correct the bug.
- Run all tests - You then run all of the tests to make sure your new code hasn't introduced new errors.
- Refactor - With your test safely passing you can safely refactor your code to improve the quality of the code.
This process is then repeated for each additional feature or bug.
There are several characteristics that you want to strive for when creating tests.
Characteristic | Description |
---|---|
Cohesive | The test only tests one thing. That doesn't mean it doesn't require other code to execute before the test asserts are made, but the test should not be making unrelated assertions. Instead create an additional test. |
Quick | You want your tests to run as quickly as possible. Ideally, you would write a little bit of production code and then run all the tests so that you can quickly discover if you broke something. When tests take a long time to run you will be discouraged from executing them. |
Do not repeat | You are not exercising the same code over and over again with different tests. |
Stable | If the test passes once, and the production code does not change, then the test should always pass. Unstable tests suggest a problem with the production or testing code. Unstable tests decrease the value of your tests because you no longer trust them, or have to rerun them multiple times to get them to pass. |
Automated | No human should be involved in the execution process of the tests. Automation allows you to run the tests as part of your continuous delivery pipeline or check in process. |
Easy | It should be easy to introduce new tests. If the process requires significant effort then it discourages people from writing them. |
JUnit is a common library that is used for testing Java code. JUnit uses a combination of annotations and assertion functions to provide its basic functionality.
When JUnit starts up it scans the code for any function that has a @Test
annotation and marks it as a unit test. Once all the tests have been discovered, JUnit executes each function. Usually your test will have one or more assertion functions that assert that your code is working correctly. JUnit provides the ability to assert that something is true, not null, equals something, or that it throws an exception. Take some time to get familiar with all the assertions functions that JUnit provides.
If any assertion fails, an exception will be thrown and that test is aborted and marked as failing. The following is an example of a JUnit test with some trivial assertions.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class ExampleTests {
@Test
public void simpleAssertionTest() {
assertEquals(200, 100 + 100);
assertTrue(100 == 2 * 50);
assertNotNull(new Object(), "Response did not return authentication String");
assertThrows(InvalidArgumentException.class, () -> {
throw new InvalidArgumentException();
});
}
}
In addition to annotating a function as a unit test, JUnit has several other useful annotations.
Annotation | Description |
---|---|
AfterAll | Run after all tests execute. |
AfterEach | Run after each test executes. |
BeforeAll | Run before all tests execute. |
BeforeEach | Run before each test executes. |
Disabled | Disable this test. Use this when you are working on a test that is not yet ready for production. |
DisplayName | The name to show for the unit test. This will default to the function name if not specified. |
Order | The order that tests should execute in. Use this if your tests have dependencies between tests. |
Test | A simple unit test. |
ParameterizedTest | A parameterized unit test. This test is called once for each supplied parameter. |
When testing interfaces it is desireable to create one set of tests for all implementations of the interface instead of duplicating the tests for each implementation. In order to accomplish this you care a ParameterizedTest
instead of a simple Test
. There are different ways to parameterize a test using JUnit, but one way is to use the ValueSource
annotation to provide multiple alternate values to pass into the test. JUnit then repeatedly invokes the test with the different provided values.
In the following example we parameterize our test to pass in different implementations of the List
interface. The test function is called three times, once for an ArrayList, once for a LinkedList, and once for a Stack.
@ParameterizedTest
@ValueSource(classes = {ArrayList.class, LinkedList.class, Stack.class})
public void addAndGetToList(Class<? extends List> listClass) throws Exception {
var list = listClass.getDeclaredConstructor().newInstance();
var expectedItem = "item";
list.add(expectedItem);
Assertions.assertEquals(1, list.size());
Assertions.assertEquals(expectedItem, list.get(0));
}
With IntelliJ, you can autogenerate your unit tests. To use this tool open up any class and select the generate|tests
option from the right click menu.
- Why unit testing is important
- How to write unit tests using the JUnit testing framework
- How to run Junit tests from Intellij
- Special considerations for testing database code
- 🎥 Why We Need Unit Testing (4:30) - [transcript]
- 🎥 Unit Testing Overview (5:10) - [transcript]
- 🎥 The JUnit Testing Framework (15:25) - [transcript]
📁 main
📁 test