The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Spring
Test code
What is test code, and who decides whether a test passes or fails?
Test code is code that writes down "if you give this method this input, it must give this output." The CalcTest class in this lab is an example. Its adds() method calls add(2, 3) of the Calc class, and assertEquals(5, ...) compares the value that add returned with the expected value, 5. If the two values differ, an error occurs on that line. The @Test above adds() is a marker that says "this method is a test." It is not we who run this code, but JUnit, a testing tool. JUnit finds the methods that have @Test and calls them. If a method ends without an error, JUnit counts it as passed. If it ends with the error that assertEquals threw, JUnit counts it as failed. In this lab you break add, write the expected value wrongly, or remove @Test, and run the same code with real JUnit each time, and you see how the number of tests found, the verdict of pass or fail, and the failure message change. (The code on the screen leaves out the import lines.)
Checked against real execution · Compiled 8 combinations on JDK 21 and JUnit 6.0.3 and ran them with the JUnit Launcher, and checked the number of tests found, passed and failed, and the failure message against the results
Concepts in this lab
- Test code
- Test code is code that is written to check whether other code works correctly. It writes down "if you give this method this input, it must give this output" as code, and it has a line (an assertion) that compares the value that actually came out with the expected value. In JUnit, a class that has test methods is called a test class, and an instance method that has @Test is called a test method. It is not the main we wrote but a testing tool such as JUnit that runs test code, and after the tool calls a test method, it counts the test as passed or failed. If no test is run at all, nothing has been checked even when there are 0 failures, so you should also look at the number of tests that were run.
- JUnit
- JUnit is a testing tool that is widely used to write and run Java test code. JUnit Jupiter, part of it, combines the way to write tests (the programming model) and the way to extend them (the extension model), and @Test and the Assertions class belong to it. When a development tool or a build tool runs JUnit, JUnit finds the test methods in the test classes, calls them, and counts a result such as passed or failed for each test. The verdict is made by JUnit, not by a person reading the output. In the JUnit 6.0.3 that was run, a test that ended without an error was counted as a success, and a test that ended with an error thrown by an assertion was counted as a failure.
- @Test
- @Test is an annotation that you put above a method to tell JUnit that the method is a test method. According to the documentation, a method with @Test must not be private or static, and must not return a value. JUnit counts a method that has @Test (or an annotation such as @RepeatedTest or @ParameterizedTest) as a test, so a method without the marker is not counted as a test even if its content looks like a test. In the JUnit 6.0.3 that was run, when a class that has a method without @Test was run, the number of tests found was 0, and the method was never run, with no error and no failure.
- assertEquals
- assertEquals(expected, actual) is a static method of the Assertions class of JUnit Jupiter, and it asserts that the first argument (the expected value) and the second argument (the actual value) are equal. If they are equal, it finishes without anything happening, and if they differ, it throws an error, and the test method ends on that line. The documentation says that a failed assertion throws AssertionFailedError or a subtype of it unless something else is specified, and in the JUnit 6.0.3 that was run, the message came out in the form expected: <5> but was: <-1>. It only tells you that the two values differ and not which of them is wrong, so a person reads the failure message and decides whether the code or the expected value is at fault.
Try these
- Default settings — adds(), which has @Test, compares the result of add(2, 3) with the expected value 5. They are equal, so the test passes.
- If you break add into a - b — The result of add(2, 3) becomes -1, which differs from the expected value 5, and JUnit counts the test as failed.
- If you write 6 as the expected value — Even though the code is correct, it differs from the expectation that was written down, so the test fails.
- If you remove @Test — JUnit does not count adds() as a test, so it never runs it.
- If you change it to a - b and also remove @Test — Even though the code is wrong, the test is not run, so you get no notice at all.
- If it is a - b and the expected value is also 6 — Even though both are wrong, JUnit only tells you that the two values differ.
Running with the default settings
- JUnit, a testing tool, runs the CalcTest class. JUnit is a program that finds tests, calls them, and judges whether each one passed or failed. The code we wrote has no main method and no line that calls the adds() method. It is JUnit that moves the code.
- JUnit looks at the methods of the CalcTest class and finds the ones that have @Test. adds() has @Test, so JUnit counts this method as a test. The number of tests found is 1.
- JUnit calls the adds() method. We did not write this line either. It is not our code but JUnit that calls the test method.
- new Calc().add(2, 3) inside adds() runs. It creates a Calc object and passes 2 and 3 to add, and the line return a + b; calculates the result (2 + 3).
- The result that add calculated is 5. add returns this value to adds(), and it is the actual value.
- assertEquals(5, ...) compares the two values. The expected value is 5 and the actual value is 5. The two values are equal, so assertEquals finishes without anything happening, and adds() also goes through to the end without an error.
- JUnit sees that adds() ended without an error and counts it as passed. It was JUnit that judged the pass, not a person reading the output with their eyes.
- JUnit finishes. Tests found: 1, passed: 1, failed: 0.
Result of this run
- Test code is code made of the method adds(), which calls Calc, and a line that compares the result with assertEquals(expected value, actual value). What differs from ordinary code is the @Test above the method (the marker that JUnit looks for) and assertEquals (the line on which JUnit judges). The two values were equal, so adds() ended without an error, and JUnit counted it as passed.
- We did not write the line that calls adds(). Finding the test, calling it, and judging whether it passed is done by JUnit. Passed means "ended without an error."
Code
// Code under test
class Calc {
int add(int a, int b) {
return a + b;
}
}
// Test code: code that checks whether Calc works correctly
class CalcTest {
// Marker: JUnit finds the methods that have @Test and runs them as tests
@Test
void adds() {
// Verdict: if the first value (expected) and the second value (actual) differ, the test fails
assertEquals(5, new Calc().add(2, 3));
}
}
Assumptions of this model
What was run is JDK 21.0.12.1 (javac 21), JUnit Jupiter 6.0.3, and JUnit Platform Launcher 6.0.3. All 8 combinations were checked against the following: the same source as the code on the screen, with only two import lines added at the top (org.junit.jupiter.api.Test and Assertions.assertEquals), was compiled with javax.tools, and the CalcTest class was run by calling the JUnit Platform Launcher from code to get a summary. The ConsoleLauncher was not used from a terminal; the Launcher API of the same tool was used directly. What was checked is the number of tests found, the number started, the numbers of successes and failures, the kind of exception that failed (org.opentest4j.AssertionFailedError), and the first line of its message. The message text expected: <5> but was: <-1> is exactly what this JUnit actually produced. For the facts that the step sentences rely on, a separate source with fields that count calls was run in the same way, and it was confirmed that with @Test, adds() and add each run once without any line of our code calling them, and that with @Test removed, both run 0 times. For the documentation, I opened the following. In the Definitions section of the JUnit User Guide (docs.junit.org, version 6.1.3), I checked that a test method is an instance method annotated with @Test and so on, and that a test class is a class that has test methods. In the Test Classes and Methods section, I checked that test classes and methods do not have to be public but must not be private, and must not return a value. In the Assertions section, I checked that the assertions of JUnit Jupiter are static methods of org.junit.jupiter.api.Assertions, and that the example writes the expected value first, as in assertEquals(2, calculator.add(1, 1)). In the API documentation, I checked that @Test is the marker that signals that a method is a test method and that it must not be private or static, the description of Assertions (a failed assertion throws AssertionFailedError or a subtype of it unless something else is specified), and the summary of assertEquals(int expected, int actual) (it asserts that expected and actual are equal). The documentation is version 6.1.3, and the JUnit that was run is version 6.0.3. What the documentation defines and what was observed in this run are different things. That a test is counted as passed if it ends without an error and as failed if it ends with an error, and that a method without @Test is not found, so that the number of tests found is 0 and the number of failures is also counted as 0, are behaviors observed in this JUnit (I did not run the exit code of the ConsoleLauncher or how a build tool treats 0 tests). That the first of the two values of assertEquals is the expected value was checked from the method name and the argument names (expected, actual), and the documentation does not state the order separately as a rule. The screen deals with only one test, and it does not cover naming tests, multiple assertions, setup and cleanup methods, tests that start a Spring context, fake objects such as @MockitoBean, or the build process that runs tests automatically. Tests that start a Spring context are covered in the in-depth lab "Testing", and running tests automatically every time you push is covered in the in-depth lab "CI/CD pipelines".
Previous lab: Spring Boot · Next lab: Entities