Testing

jim stafford

Introduction

Why Do We Test?

Demonstration? Verification/Validation? Bug Detection? Design? More…​?


There are many great reasons to incorporate software testing into the application lifecycle. There is no time too early to start.

What are Test Levels?

Unit Testing - Integration Testing - System Testing - Acceptance Testing - More…​?


It would be easy to say that our focus in this lesson will be on unit and integration testing. However, there are some aspects of system and acceptance testing that are applicable as well.

What are some Approaches to Testing?

Static Analysis - Dynamic Analysis - White-box Testing - Black-box Testing

Many, many more …​


In this lesson we will focus on dynamic analysis testing using both black-box interface contract testing and white-box implementation and collaboration testing.

Goals

The student will learn:

  • to understand the testing frameworks bundled within Spring Boot Test Starter

  • to leverage test cases and test methods to automate tests performed

  • to leverage assertions to verify correctness

  • to integrate mocks into test cases

  • to implement unit integration tests within Spring Boot

  • to express tests using Behavior-Driven Development (BDD) acceptance test keywords

  • to automate the execution of tests using Maven

  • to augment and/or replace components used in a unit integration test

Objectives

At the conclusion of this lecture and related exercises, the student will be able to:

  1. write a test case and assertions using "Vintage" JUnit 4 constructs

  2. write a test case and assertions using JUnit 5 "Jupiter" constructs

  3. leverage alternate (JUnit, Hamcrest, AssertJ, etc.) assertion libraries

  4. implement a mock (using Mockito) into a JUnit unit test

    1. define custom behavior for a mock

    2. capture and inspect calls made to mocks by subjects under test

  5. implement BDD acceptance test keywords into Mockito & AssertJ-based tests

  6. implement unit integration tests using a Spring context

  7. implement (Mockito) mocks in Spring context for unit integration tests

  8. augment and/or override Spring context components using @TestConfiguration

  9. execute tests using Maven Surefire plugin

Test Constructs

At the heart of testing, we want to

  • establish a subject under test

  • establish a context in which to test that subject

  • perform actions on the subject

  • evaluate the results

apptesting tests
Figure 1. Basic Test Concepts

Subjects can vary in scope depending on the type of our test

  • Unit testing will have class and method-level subjects

  • Integration tests can span multiple classes/components — whether

    • vertically (e.g., front-end request to database)

    • horizontally (e.g., peers)

Automated Test Terminology

You will see terms "unit" and "integration" used differently as we go through the testing topics and span tooling

  • Conceptually

    • unit tests dealing with one subject at a time and involve varying levels of simulation around them in order to test that subject

    • integration tests when multiple real components brought together to form the overall set of subjects — whether that be vertical (e.g., to the database and back) or horizontal (e.g., peer interactions) in nature.

  • Test Management - worry about what it takes to spin up and shutdown resources to conduct our testing

    • Maven refers to unit tests as anything that can be performed within a single JVM

    • Maven refers to integration tests as tests that require managing external resources (e.g., start/stop web server).

Maven Test Types

Maven runs these tests in different phases

By default

Test Naming Conventions

  • neither tools like JUnit or the IDEs care how are classes are named

  • however to automate testing — we have to pay attention to Maven Surefire and Failsafe naming rules

Lecture Test Naming Conventions

Unit Test - conceptual unit test focused on a limited subject

  • will use the suffix "Test".

  • run without a Spring context and will be picked up by Maven Surefire

Unit Integration Test - conceptual integration test (vertical or horizontal) runnable within a single JVM

  • will use the suffix "NTest"

  • will likely involve a Spring context and will be picked up by Maven Surefire

External Integration Test - conceptual integration test (vertical or horizontal) requiring external resource management

  • will use the suffix "IT"

  • will always have Spring context(s) running in one or more JVMs and will be picked up by Maven Failsafe

You will see conceptual integration tests executed during Maven Surefire test phase if we can perform testing without resource management of external processes

Spring Boot Starter Test Frameworks

We want to automate tests as much as possible

  • can do that with many of the Spring Boot testing options

  • made available using spring-boot-starter-test dependency

    • transitive dependencies on several powerful, state of the art as well as legacy, testing frameworks.

  • only used during builds and not in production

    • assign a scope of test to this dependency

pom.xml spring-boot-test-starter Dependency
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope> (1)
        </dependency>
1dependency scope is test since these dependencies are not required to run outside of build environment

Spring Boot Starter Transitive Dependencies

spring-boot-starter-test Transitive Dependencies (reorganized)
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.3.2:test
[INFO] |  +- org.springframework.boot:spring-boot-test:jar:3.3.2:test
[INFO] |  +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.3.2:test
[INFO] |  +- org.springframework:spring-test:jar:6.1.11:test

[INFO] |  +- org.junit.jupiter:junit-jupiter:jar:5.10.3:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-api:jar:5.10.3:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-params:jar:5.10.3:test
[INFO] |  |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.10.3:test

[INFO] |  +- org.assertj:assertj-core:jar:3.25.3:test
[INFO] |  |  \- net.bytebuddy:byte-buddy:jar:1.14.18:test
[INFO] |  +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] +- org.exparity:hamcrest-date:jar:2.0.8:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:2.2:test

[INFO] |  +- org.mockito:mockito-core:jar:5.11.0:test
[INFO] |  |  +- net.bytebuddy:byte-buddy-agent:jar:1.14.18:test
[INFO] |  |  \- org.objenesis:objenesis:jar:3.3:test
[INFO] |  +- org.mockito:mockito-junit-jupiter:jar:5.11.0:test

[INFO] |  +- com.jayway.jsonpath:json-path:jar:2.9.0:test
[INFO] |  |  \- org.slf4j:slf4j-api:jar:2.0.13:compile

[INFO] |  +- org.skyscreamer:jsonassert:jar:1.5.3:test
[INFO] |  \- org.xmlunit:xmlunit-core:jar:2.9.1:test

[INFO] \- org.junit.vintage:junit-vintage-engine:jar:5.10.3:test
[INFO]    +- org.junit.platform:junit-platform-engine:jar:1.10.3:test
[INFO]    +- junit:junit:jar:4.13.2:test
...

Transitive Dependency Test Tools

At a high level:

  • spring-boot-test-autoconfigure - contains many auto-configuration classes that detect test conditions and configure common resources for use in a test mode

  • junit - required to run the JUnit tests

  • hamcrest - required to implement Hamcrest test assertions

  • assertj - required to implement AssertJ test assertions

  • mockito - required to implement Mockito mocks

  • jsonassert - required to write flexible assertions for JSON data

  • jsonpath - used to express paths within JSON structures

  • xmlunit - required to write flexible assertions for XML data

In the rest of this lesson, I will be describing how JUnit, the assertion libraries, Mockito and Spring Boot play a significant role in unit and integration testing.

JUnit Background

  • tests that perform actions on the subjects within a given context and assert proper results

  • test cases that group tests and wrap in a set of common setup and teardown steps

  • test suites that provide a way of grouping certain tests

Test Suites are not as pervasive as test cases and tests
testing junit constructs
Figure 2. Basic JUnit Test Framework Constructs

JUnit - API Annotations

  • annotations added in Java 5 permitted frameworks to move away from inheritance-based approaches — with specifically named methods (JUnit 3.8) and to leverage annotations added to classes and methods (JUnit 4)

public class MathTest extends TestCase {
    protected void setUp() { } (1)
    protected void tearDown() { } (1)
    public void testAdd() { (2)
       assertEquals(4, 2+2);
    }
}
1setUp() and tearDown() are method overrides of base class TestCase
2all test methods were required to start with word test
public class MathTest {
    @Before
    public void setup() { } (1)
    @After
    public void teardown() { } (1)
    @Test
    public void add() { (1)
       assertEquals(4, 2+2);
    }
}
1public methods found by annotation — no naming requirement

JUnit API - Lambdas

  • lamda functions (JUnit 5/Jupiter) added in Java 8 permit the flexible expression of code blocks that can extend the behavior of provided functionality without requiring verbose subclassing

JUnit 4/Vintage Assertions
assertEquals(5,2+2);//fails here
assertEquals(3,2+2);//not eval (1)
assertEquals(String.format("try%d",2), 4,2+2); (2)
1evaluation will stop at first failure
2descriptions were first parameter and always evaluated
JUnit 5/Jupiter Lambda Assertions
assertAll(//all get eval and reported (1)
    () -> assertEquals(5,2+2),
    () -> assertEquals(3,2+2),
    () -> assertEquals(4,2+2, (2) (3)
        ()->String.format("try%d",2))
);
1all assertions can be evaluated
2descriptions are now last argument
3only evaluated if fail with lambdas

JUnit 5 Evolution

The success and simplicity of JUnit 4 made it hard to incorporate new features. JUnit 4 was a single module/JAR and everything that used JUnit leveraged that single jar.

apptesting junit4

JUnit 5 Areas

The next iteration of JUnit involved a total rewrite — that separated the overall project into three (3) modules.

  • JUnit Platform

  • JUnit Jupiter ("new stuff")

    • evolution from legacy

    • provides TestEngine for running Jupiter-based tests

  • JUnit Vintage ("legacy stuff")

    • provides TestEngine for running JUnit 3 and JUnit 4-based tests

apptesting junit modules
Figure 4. JUnit 5 Modularization
The name Jupiter was selected because it is the 5th planet from the Sun

JUnit 5 Module JARs

The JUnit 5 modules have several JARs within them that separate interface from implementation — ultimately decoupling the test code from the core engine.

apptesting junit
Figure 5. JUnit 5 Module Contents

Syntax Basics

  • JUnit

  • Mockito

  • Spring Boot

JUnit Vintage Basics

  • backwards compatibility support for legacy JUnit 4-based tests

JUnit Vintage Example Lifecycle Methods

package info.ejava.examples.app.testing.testbasics.vintage;

import lombok.extern.slf4j.Slf4j;
import org.junit.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

@Slf4j
public class ExampleUnit4Test {
    @BeforeClass
    public static void setUpClass() {
        log.info("setUpClass");
    }
    @Before
    public void setUp() {
        log.info("setUp");
    }
    @After
    public void tearDown() {
        log.info("tearDown");
    }
    @AfterClass
    public static void tearDownClass() {
        log.info("tearDownClass");
    }
  • annotations come from the org.junit.* Java package

  • lifecycle annotations are

    • @BeforeClass — a public static method run before the first @Before method call and all tests within the class

    • @Before - a public instance method run before each test in the class

    • @After - a public instance method run after each test in the class

    • @AfterClass - a public static method run after all tests within the class and the last @After method called

JUnit Vintage Example Test Methods

Basic JUnit Vintage Example Test Methods
@Test(expected = IllegalArgumentException.class)
public void two_plus_two() {
    log.info("2+2=4");
    assertEquals(4,2+2);
    throw new IllegalArgumentException("just demonstrating expected exception");
}
@Test
public void one_and_one() {
    log.info("1+1=2");
    assertTrue("problem with 1+1", 1+1==2);
    assertEquals(String.format("problem with %d+%d",1,1), 2, 1+1);
}
  • @Test - a public instance method where subjects are invoked and result assertions are made

  • exceptions can be asserted at overall method level — but not at a specific point in the method and exception itself cannot be inspected without switching to a manual try/catch technique

  • asserts can be augmented with a String message in the first position

    • the expense of building String message is always paid whether needed or not

      assertEquals(String.format("problem with %d+%d",1,1), 2, 1+1);

Vintage requires the class and methods have public access.

JUnit Vintage Basic Syntax Example Output

Basic JUnit Vintage Example Output
16:35:42.293 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - setUpClass (1)
16:35:42.297 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - setUp (2)
16:35:42.297 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - 2+2=4
16:35:42.297 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - tearDown (2)
16:35:42.299 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - setUp (2)
16:35:42.300 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - 1+1=2
16:35:42.300 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - tearDown (2)
16:35:42.300 INFO ...testing.testbasics.vintage.ExampleJUnit4Test - tearDownClass (1)
1@BeforeClass and @AfterClass called once per test class
2@Before and @After executed for each @Test


Not demonstrated — a new instance of the test class is instantiated for each test. No object state is retained from test to test without the manual use of static variables.
JUnit Vintage provides no construct to dictate repeatable ordering of test methods within a class — thus making it hard to use test cases to depict lengthy, deterministically ordered scenarios.

JUnit Jupiter Basics

To simply change-over from Vintage to Jupiter syntax, there are a few minor changes.

  • annotations and assertions have changed packages from org.junit to org.junit.jupiter.api

  • lifecycle annotations have changed names

  • assertions have changed the order of optional arguments

  • exceptions can now be explicitly tested and inspected within the test method body


Vintage no longer requires classes or methods to be public. Anything non-private should work.

JUnit Jupiter Example Lifecycle Methods

package info.ejava.examples.app.testing.testbasics.jupiter;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
class ExampleJUnit5Test {
    @BeforeAll
    static void setUpClass() {
        log.info("setUpClass");
    }
    @BeforeEach
    void setUp() {
        log.info("setUp");
    }
    @AfterEach
    void tearDown() {
        log.info("tearDown");
    }
    @AfterAll
    static void tearDownClass() {
        log.info("tearDownClass");
    }
  • annotations come from the org.junit.jupiter.* Java package

  • lifecycle annotations are

    • @BeforeAll — a static method run before the first @BeforeEach method call and all tests within the class

    • @BeforeEach - an instance method run before each test in the class

    • @AfterEach - an instance method run after each test in the class

    • @AfterAll - a static method run after all tests within the class and the last @AfterEach method called

JUnit Jupiter Example Test Methods

@Test
void two_plus_two() {
    log.info("2+2=4");
    assertEquals(4,2+2);
    Exception ex=assertThrows(IllegalArgumentException.class, () ->{
        throw new IllegalArgumentException("just demonstrating expected exception");
    });
    assertTrue(ex.getMessage().startsWith("just demo"));
}
@Test
void one_and_one() {
    log.info("1+1=2");
    assertTrue(1+1==2, "problem with 1+1");
    assertEquals(2, 1+1, ()->String.format("problem with %d+%d",1,1));
}
  • @Test - a instance method where assertions are made

  • exceptions can now be explicitly tested at a specific point in the test method — permitting details of the exception to also be inspected

  • asserts can be augmented with a String message in the last position

    • this is a breaking change from Vintage syntax

    • the expense of building complex String messages can be deferred to a lambda function

      assertEquals(2, 1+1, ()→String.format("problem with %d+%d",1,1));

JUnit Jupiter Basic Syntax Example Output

16:53:44.852 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - setUpClass (1)
(3)
16:53:44.866 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - setUp (2)
16:53:44.869 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - 2+2=4
16:53:44.874 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - tearDown (2)
(3)

16:53:44.879 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - setUp (2)
16:53:44.880 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - 1+1=2
16:53:44.881 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - tearDown (2)
(3)
16:53:44.883 INFO ...testing.testbasics.jupiter.ExampleJUnit5Test - tearDownClass (1)
1@BeforeAll and @AfterAll called once per test class
2@Before and @After executed for each @Test
3The default IDE logger formatting added the new lines in between tests


Not demonstrated — we have the default option to have a new instance per test like Vintage or same instance for all tests and a defined test method order — which allows for lengthy scenario tests to be broken into increments. See @TestInstance annotation and TestInstance.Lifecycle enum for details.

JUnit Jupiter Test Case Adjustments

Test Instance

  • state used by tests can be expensive to create or outside the scope of individual tests

  • state can be initialized and shared between test methods using one of two test instance @TestInstance techniques

Shared Static State - PER_METHOD (default)

  • instance of the class torn down and re-instantiated between each test

  • must declare shared state as static

  • @BeforeAll and @AfterAll methods must be declared static

@TestInstance(TestInstance.Lifecycle.PER_METHOD) //the default (1)
class StaticShared {
    private static int staticState; (2)
    @BeforeAll
    static void init() { (3)
        log.info("state={}", staticState++);
    }
    @Test
    void testA() { log.info("state={}", staticState); }  (4)
    @Test
    void testB() { log.info("state={}", staticState); }
1test case class is instantiated per method
2any shared state must be declared private
3@BeforeAll and @AfterAll methods must be declared static
4@Test methods are normal instance methods with access to the static state

Shared Instance State - PER_CLASS

  • often during integration tests, shared state (e.g., injected components) is only available once test case is instantiated

  • make test case injectable by the container

@TestInstance(TestInstance.Lifecycle.PER_CLASS) (1)
class InstanceShared {
    private int instanceState; (2)
    @BeforeAll
    void init() { (3)
        log.info("state={}", instanceState++);
    }
    @Test
    void testA() { log.info("state={}", instanceState); }
    @Test
    void testB() { log.info("state={}", instanceState); }
1one instance is created for all tests
2any shared state must be declared private
3@BeforeAll and @AfterAll methods must be declared non-static
Use of @ParameterizedTest and @MethodSource (later topics), where the method source uses components injected into the test case instance is one example of when one needs to use PER_CLASS.

Test Ordering

  • Random Order

  • Specified Order

  • by Method Name

  • by Display Name

  • (custom order)

...
import org.junit.jupiter.api.*;

@TestMethodOrder(
//        MethodOrderer.OrderAnnotation.class
//        MethodOrderer.MethodName.class
//        MethodOrderer.DisplayName.class
          MethodOrderer.Random.class
)
class ExampleJUnit5Test {
    @Test
    @Order(1)
    void two_plus_two() {
        ...
    @Test
    @Order(2)
    void one_and_one() {

Explicit Method Ordering is the Exception

It is best practice to make test cases and tests within test cases modular and independent of one another. To require a specific order violates that practice — but sometimes there are reasons to do so. One example violation is when the overall test case is broken down into test methods that addresses a multi-step scenario. In older versions of JUnit — that would have been required to be a single @Test calling out to helper methods.

Assertion Basics

  • test case setup methods (@BeforeAll and @BeforeEach) and early parts of test method (@Test) allow us to define test context and scenario for subject of test

  • assertions are added to evaluation portion of test method to determine whether subject performed correctly

    • result of the assertions determine the pass/fail of the test

apptesting assertions
Figure 6. Assertions are Key Point in Tests

Assertion Libraries

  • JUnit - has built-in, basic assertions like True, False, Equals, NotEquals, etc.

    • Vintage - original assertions

    • Jupiter - same basic assertions with some new options and parameters swapped

  • Hamcrest - uses natural-language expressions for matches

  • AssertJ - an improvement to natural-language assertion expressions using type-based builders

The built-in JUnit assertions are functional enough to get any job done. The value in using the other libraries is their ability to express the assertion using natural-language terms without using a lot of extra, generic Java code.

JUnit Assertions

  • basic and easy to understand — but limited in their expression

  • basic form of taking subject argument(s) and name of static method is the assertion made about the arguments

Example JUnit Assertion
import static org.junit.jupiter.api.Assertions.*;
...
        assertEquals(expected, lhs+rhs); (1)
1JUnit static method assertions express assertion of one to two arguments


  • limited by the number of static assertion methods present

  • have to extend them by using code to manipulate the arguments (e.g., to be equal or true/false)

  • can easily bring in robust assertion libraries — exactly what JUnit User Guide describes to do

Hamcrest Assertions

  • common pattern of taking a subject argument and a Matcher argument

Example Hamcrest Assertion
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

...
        assertThat(beaver.getFirstName(), equalTo("Jerry")); (1)
1LHS argument is value being tested, RHS equalTo returns an object implementing Matcher interface


  • Matcher interface can be implemented by unlimited number of expressions to implement details of assertion

AssertJ Assertions

  • uses builder pattern that starts with the subject — then offers a nested number of assertion builders that are based on the previous node type

import static org.assertj.core.api.Assertions.assertThat;

...
        assertThat(beaver.getFirstName()).isEqualTo("Jerry"); (1)
1assertThat is a builder of assertion factories and isEqual executes an assertion in chain

Custom AssertJ Assertions

import static info.ejava.examples.app.testing.testbasics.Assertions.*;
...
        assertThat(beaver).hasFirstName("Jerry");
IDEs have an easier time suggesting assertion builders with AssertJ because everything is a method call on the previous type. IDEs have a harder time suggesting Hamcrest matchers because there is very little to base the context on.

AssertJ Generator and Jakarta

  • Assertj core library has kept up to date

  • assertions generator plugin has not kept up

    • Current default execution of plugin results in classes annotated with javax.annotation.Generated annotation that has since been changed to jakarta

  • no details here, but class example in gitlab shows


Assertj support ticket indicates they are working on it as a part of a Java 17 upgrade

Example Library Assertions

package info.ejava.examples.app.testing.testbasics.jupiter;

import lombok.extern.slf4j.Slf4j;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

@Slf4j
class AssertionsTest {
    int lhs=1;
    int rhs=1;
    int expected=2;

    @Test
    void one_and_one() {
        //junit 4/Vintage assertion
        Assert.assertEquals(expected, lhs+rhs); (1)
        //Jupiter assertion
        Assertions.assertEquals(expected, lhs+rhs); (1)
        //hamcrest assertion
        MatcherAssert.assertThat(lhs+rhs, Matchers.is(expected)); (2)
        //AssertJ assertion
        org.assertj.core.api.Assertions.assertThat(lhs+rhs).isEqualTo(expected); (3)
    }
}
1JUnit assertions are expressed using a static method and one or more subject arguments
2Hamcrest asserts that the subject matches a Matcher that can be infinitely extended
3AssertJ’s extensible subject assertion provides type-specific assertion builders

Assertion Failures

  • report generic message of the assertion failure (location not shown) without context other than the test case and test method it was generated from (not shown)


Example Default Assert Failure Message
java.lang.AssertionError: expected:<3> but was:<2> (1)
1we are not told what 3 and 2 are within a test except that 3 was expected and they are not equal

Adding Assertion Context

  • additional text can sometimes help provide more context about failure

@Test
void one_and_one_description() {
    //junit 4/Vintage assertion
    Assert.assertEquals("math error", expected, lhs+rhs); (1)
    //Jupiter assertions
    Assertions.assertEquals(expected, lhs+rhs, "math error"); (2)
    Assertions.assertEquals(expected, lhs+rhs,
            ()->String.format("math error %d+%d!=%d",lhs,rhs,expected)); (3)
    //hamcrest assertion
    MatcherAssert.assertThat("math error",lhs+rhs, Matchers.is(expected)); (4)
    //AssertJ assertion
    org.assertj.core.api.Assertions.assertThat(lhs+rhs)
            .as("math error") (5)
            .isEqualTo(expected);
    org.assertj.core.api.Assertions.assertThat(lhs+rhs)
            .as("math error %d+%d!=%d",lhs,rhs,expected) (6)
            .isEqualTo(expected);
}
1JUnit Vintage syntax places optional message as first parameter
2JUnit Jupiter moves the optional message to the last parameter
3JUnit Jupiter also allows optional message to be expressed thru a lambda function
4Hamcrest passes message in first position like JUnit Vintage syntax
5AspectJ uses an as() builder method to supply a message
6AspectJ also supports String.format and args when expressing message

Assertion Failure Context Output

Example Assert Failure with Supplied Message
java.lang.AssertionError: math error expected:<3> but was:<2> (1)
1an extra "math error" was added to the reported error to help provide context


Although AssertJ supports multiple asserts in a single call chain, your description (as("description")) must come before the first failing assertion.


Because AssertJ uses chaining

  • there are fewer imports required

  • IDEs are able to more easily suggest a matcher based on the type returned from the end of the chain. There is always a context specific to the next step.

Testing Multiple Assertions

  • evaluation stops at first failure in test method

  • there are many times when we want to know the results of several assertions

    • example: testing different fields in a returned object (e.g., person.getFirstName(), person.getLastName())

  • more complete results may give us better insight for the entire problem


  • JUnit Jupiter and AssertJ support testing multiple assertions prior to failing a specific test and then go on to report the results of each failed assertion

JUnit Jupiter Multiple Assertion Support

JUnit Jupiter Multiple Assertion Support
@Test
void junit_all() {
    Assertions.assertAll("all assertions",
        () -> Assertions.assertEquals(expected, lhs+rhs, "jupiter assertion"), (1)
        () -> Assertions.assertEquals(expected, lhs+rhs,
            ()->String.format("jupiter format %d+%d!=%d",lhs,rhs,expected))
    );
}
1JUnit Jupiter uses Java 8 lambda functions to execute and report results for multiple assertions

AssertJ Multiple Assertion Support

  • special factory class (SoftAssertions) to build assertions that do not immediately fail test

  • have chance to inspect state of assertions before failing the test

    • gives us chance to gather additional information to place into log

  • have the option not to technically fail the test

AssertJ Multiple Assertion Support
import org.assertj.core.api.SoftAssertions;
...
    @Test
    public void all() {
        Person p = beaver; //change to eddie to cause failures
        SoftAssertions softly = new SoftAssertions(); (1)
        softly.assertThat(p.getFirstName()).isEqualTo("Jerry");
        softly.assertThat(p.getLastName()).isEqualTo("Mathers");
        softly.assertThat(p.getDob()).isAfter(wally.getDob());

        log.info("error count={}", softly.errorsCollected().size()); (2)
        softly.assertAll(); (3)
    }
1a special SoftAssertions builder is used to construct assertions
2we are able to inspect the status of the assertions before failure thrown
3assertion failure thrown during later assertAll() call

Asserting Exceptions

  • JUnit Jupiter and AssertJ provide direct support for inspecting Exceptions within the body of the test method

  • surprisingly, Hamcrest offers no built-in matchers to directly inspect Exceptions

JUnit Jupiter Exception Handling Support

  • JUnit Jupiter allows for explicit testing for Exceptions at specific points within the test method

  • the type of Exception is checked and made available to follow-on assertions to inspect

    • from that point forward JUnit assertions do not provide any direct support to inspect the Exception

JUnit Jupiter Exception Handling Support
import org.junit.jupiter.api.Assertions;
...
    @Test
    public void exceptions() {
        RuntimeException ex1 = Assertions.assertThrows(RuntimeException.class, (1)
            () -> {
                throw new IllegalArgumentException("example exception");
            });
    }
1JUnit Jupiter provides means to assert an Exception thrown and provide it for inspection

AssertJ Exception Handling Support

  • AssertJ has an Exception testing capability similar to JUnit Jupiter

    • can make explicit check for the Exception to be thrown and make Exception available for inspection

  • difference — AssertJ provides assertions to directly inspect Exceptions using natural-language calls

AssertJ Exception Handling and Inspection Support
Throwable ex1 = catchThrowable( (1)
        ()->{ throw new IllegalArgumentException("example exception"); });
assertThat(ex1).hasMessage("example exception"); (2)

RuntimeException ex2 = catchThrowableOfType( (1)
        ()->{ throw new IllegalArgumentException("example exception"); },
        RuntimeException.class);
assertThat(ex1).hasMessage("example exception"); (2)
1AssertJ provides means to assert an Exception thrown and provide it for inspection
2AssertJ provides assertions to directly inspect Exceptions

AssertJ Exception Handling Support (cont.)

  • AssertJ also provides assertions that not only check that exception thrown — but can also tack on assertion builders to make on-the-spot assertions about exception thrown

  • same end functionality as the previous example — except:

    • previous method returned the exception thrown

    • this technique returns an assertion builder

AssertJ Integrated Exception Handling Support
assertThatThrownBy( (1)
        () -> {
            throw new IllegalArgumentException("example exception");
        }).hasMessage("example exception");

assertThatExceptionOfType(RuntimeException.class).isThrownBy( (1)
        () -> {
            throw new IllegalArgumentException("example exception");
        }).withMessage("example exception");
1AssertJ provides means to use the caught Exception as an assertion factory to directly inspect the Exception in a single chained call

Asserting Dates

  • AssertJ has built-in support for date assertions

  • We have to add a separate library to gain date matchers for Hamcrest

AssertJ Date Handling Support

AssertJ Exception Handling Support
import static org.assertj.core.api.Assertions.*;
...
    @Test
    public void dateTypes() {
        assertThat(beaver.getDob()).isAfter(wally.getDob());
        assertThat(beaver.getDob())
             .as("beaver NOT younger than wally")
             .isAfter(wally.getDob()); (1)
    }
1AssertJ builds date assertions that directly inspect dates using natural-language

Hamcrest Date Handling Support

external hamcrest-date library adds natural-language date support

Hamcrest Date Support Dependency
<!-- for hamcrest date comparisons -->
<dependency>
    <groupId>org.exparity</groupId>
    <artifactId>hamcrest-date</artifactId>
    <version>2.0.7</version>
    <scope>test</scope>
</dependency>
  • DateMatchers class supplies date matchers to express date assertions

Hamcrest Date Handling Support
import org.exparity.hamcrest.date.DateMatchers;
import static org.hamcrest.MatcherAssert.assertThat;
...
    @Test
    public void dateTypes() {
        //requires additional org.exparity:hamcrest-date library
        assertThat(beaver.getDob(), DateMatchers.after(wally.getDob()));
        assertThat("beaver NOT younger than wally", beaver.getDob(),
                   DateMatchers.after(wally.getDob())); (1)
    }
1hamcrest-date adds matchers that can directly inspect dates

Mockito Basics

  • more complex software to test than what we have shown so far in this lesson

  • components inevitably structured into layered dependencies

    • one layer cannot be tested without the lower layers it calls

  • To implement unit tests, we have a few choices:

    1. use the real lower-level components (i.e., "all the way to the DB and back", remember — I am calling that choice "Unit Integration Tests")

    2. create a stand-in for the lower-level components (aka "test double")

  • likely take first approach during integration testing

    • but interfacing components may bring in unrealistic dependencies for unit testing an individual component

Test Doubles

The second approach ("test double") has a few options:

  • fake - using a scaled down version of the real component (e.g., in-memory SQL database)

  • stub - simulation of the real component by using pre-cached test data

  • mock - defining responses to calls and the ability to inspect the actual incoming calls made

Mock Support

  • spring-boot-starter-test brings in a pre-integrated, mature open source mocking framework called Mockito

  • Upcoming Example

    • unit test with a simple Java Map<String, String> used to demonstrate some simulation and inspection concepts

  • in real unit test, the Java Map interface would stand for:

Mockito Learning Example Declarations

package info.ejava.examples.app.testing.testbasics.mockito;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class ExampleMockitoTest {
    @Mock //creating a mock to configure for use in each test
    private Map<String, String> mapMock;
    @Captor
    private ArgumentCaptor<String> stringArgCaptor;
  • @ExtendWith bootstraps Mockito behavior into test case

  • @Mock can be used to inject a mock of the defined type

    • "nice mock" is immediately available - will react in potentially useful manner by default

  • @Captor can be used to capture input parameters passed to the mock calls

@InjectMocks will be demonstrated in later white box testing — where the defined mocks get injected into component under test.

Mockito Learning Example Test

The following learning example provides a demonstration of Mock capability — using only the Mock, without the component under test. This is not something anyone would do because it is only demonstrating the Mock capability versus testing the component under test using the assembled Mock. The calls to the Mock during the "conduct test" are calls we would anticipate the component under test would make while being tested.
@Test
public void listMap() {
    //define behavior of mock during test
    when(mapMock.get(stringArgCaptor.capture()))
            .thenReturn("springboot", "testing"); (1)

    //conduct test
    int size = mapMock.size();
    String secret1 = mapMock.get("happiness");
    String secret2 = mapMock.get("joy");

    //evaluate results
    verify(mapMock).size(); //verify called once  (3)
    verify(mapMock, times(2)).get(anyString()); //verify called twice
    //verify what was given to mock
    assertThat(stringArgCaptor.getAllValues().get(0)).isEqualTo("happiness"); (2)
    assertThat(stringArgCaptor.getAllValues().get(1)).isEqualTo("joy");
    //verify what was returned by mock
    assertThat(size).as("unexpected size").isEqualTo(0);
    assertThat(secret1).as("unexpected first result").isEqualTo("springboot");
    assertThat(secret2).as("unexpected second result").isEqualTo("testing");
}
1when()/then() define custom conditions and responses for mock within scope of test
2getValue()/getAllValues() can be called on the captor to obtain value(s) passed to the mock
3verify() can be called to verify what was called of the mock
mapMock.size() returned 0 while mapMock.get() returned values. We defined behavior for mapMock.get() but left other interface methods in their default, "nice mock" state.

BDD Acceptance Test Terminology

  • Behavior-Driven Development (BDD) can be part of an agile development process

    • adds natural-language constructs to express behaviors and outcomes

  • BDD behavior specifications are stories with a certain structure that contain an acceptance criteria that follows a "given", "when", "then" structure:

    • given - initial context

    • when - event triggering scenario under test

    • then - expected outcome

Alternate BDD Syntax Support

  • strong push to express acceptance criteria in code that can be executed versus a document

  • far from a perfect solution, JUnit, AssertJ, and Mockito do provide some syntax support for BDD-based testing:

    • JUnit Jupiter allows the assignment of meaningful natural-language phrases for test case and test method names. Nested classes can also be employed to provide additional expression.

    • Mockito defines alternate method names to better map to the given/when/then language of BDD

    • AssertJ defines alternate assertion factory names using then() and and.then() wording

Example BDD Syntax Support

import org.junit.jupiter.api.*;
import static org.assertj.core.api.BDDAssertions.and;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) (1)
@DisplayName("map") (2)
public class ExampleMockitoTest {
...
    @Nested (3)
    public class when_has_key { (1)
        @Test
        public void returns_values() {
            //given
            given(mapMock.get(stringArgCaptor.capture()))
                    .willReturn("springboot", "testing"); (4)
//alt syntax
//        doReturn("springboot", "testing")
//                .when(mapMock).get(stringArgCaptor.capture());
            //when
            int size = mapMock.size();
            String secret1 = mapMock.get("happiness");
            String secret2 = mapMock.get("joy");

            //then - can use static import for BDDMockito or BDDAssertions, not both
            then(mapMock).should().size(); //verify called once (5)
            then(mapMock).should(times(2)).get(anyString()); //verify called twice
            (7) (6)
            and.then(stringArgCaptor.getAllValues().get(0)).isEqualTo("happiness");
            and.then(stringArgCaptor.getAllValues().get(1)).isEqualTo("joy");
            and.then(size).as("unexpected size").isEqualTo(0);
            and.then(secret1).as("unexpected first result").isEqualTo("springboot");
            and.then(secret2).as("unexpected second result").isEqualTo("testing");
        }
    }
1JUnit DisplayNameGenorator.ReplaceUnderscores will form a natural-language display name by replacing underscores with spaces
2JUnit DisplayName sets the display name to a specific value
3JUnit Nested classes can be used to better express test context
4Mockito when/then syntax replaced by given/will syntax expresses the definition of the mock
5Mockito verify/then syntax replaced by then/should syntax expresses assertions made on the mock
6AssertJ then syntax expresses assertions made to supported object types
7AssertJ and field provides a natural-language way to access both AssertJ then and Mockito then in the same class/method
AssertJ provides a static final and field to allow its static then() and Mockito’s static then() to be accesses in the same class/test

Example BDD Syntax Output

apptesting bdd syntax
Figure 7. Example BDD Syntax Output

JUnit Options Expressed in Properties

We can define a global setting for the display name generator using junit-platform.properties

test-classes/junit-platform.properties
junit.jupiter.displayname.generator.default = \
  org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

This can also be used to express:

  • method order

  • class order

  • test instance lifecycle

  • @Parameterized test naming

  • parallel execution

Tipping Example

  • TipCalculator - returns the amount of tip required when given a certain bill total and rating of service. We could have multiple evaluators for tips and have defined an interface for clients to depend upon.

  • BillCalculator - provides the ability to calculate the share of an equally split bill given a total, service quality, and number of people.

apptesting tip classes
Figure 8. Tipping Example Class Model

Review: Unit Test Basics

  • review the simple test constructs in terms of the Tipping example

Review: POJO Unit Test Setup

Review: POJO Unit Test Setup
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) (1)
@DisplayName("Standard Tipping Calculator")
public class StandardTippingCalculatorImplTest {
    //subject under test
    private TipCalculator tipCalculator; (2)

    @BeforeEach (3)
    void setup() { //simulating a complex initialization
        tipCalculator=new StandardTippingImpl();
    }
1DisplayName is part of BDD naming and optional for all tests
2there will be one or more objects under test. These will be POJOs.
3@BeforeEach plays the role of a the container — wiring up objects under test

Review: POJO Unit Test

  • expressed in terms of BDD conventions

  • broken up into "given", "when", and "then" blocks

    • highlighted with use of BDD syntax where provided (JUnit and AssertJ)

Review: POJO Unit Test
@Test
public void given_fair_service() { (1)
    //given - a $100 bill with FAIR service (2)
    BigDecimal billTotal = new BigDecimal(100);
    ServiceQuality serviceQuality = ServiceQuality.FAIR;

    //when - calculating tip (2)
    BigDecimal resultTip = tipCalculator.calcTip(billTotal, serviceQuality);

    //then - expect a result that is 15% of the $100 total (2)
    BigDecimal expectedTip = billTotal.multiply(BigDecimal.valueOf(0.15));
    then(resultTip).isEqualTo(expectedTip); (3)
}
1using JUnit snake_case natural language expression for test name
2BDD convention of given, when, then blocks. Helps to be short and focused
3using AssertJ assertions with BDD syntax

Review: Mocked Unit Test Setup

  • subject under test has a required dependency

  • pure unit test would mock out all dependencies (TipCalculator)

Review: Mocked Unit Test Setup
@ExtendWith(MockitoExtension.class) (1)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Bill CalculatorImpl Mocked Unit Test")
public class BillCalculatorMockedTest {
    //subject under test
    private BillCalculator billCalculator;

    @Mock (2)
    private TipCalculator tipCalculatorMock;

    @BeforeEach
    void init() { (3)
        billCalculator = new BillCalculatorImpl(tipCalculatorMock);
    }
1Add Mockito extension to JUnit
2Identify which interfaces to Mock
3In this example, we are manually wiring up the subject under test

Review: Mocked Unit Test

  • TipCalculator mock instructed on what to return based on input criteria

  • mock making call activity available to the test

Review: Mocked Unit Test
@Test
public void calc_shares_for_people_including_tip() {
    //given - we have a bill for 4 people and tip calculator that returns tip amount
    BigDecimal billTotal = new BigDecimal(100.0);
    ServiceQuality service = ServiceQuality.GOOD;
    BigDecimal tip = billTotal.multiply(new BigDecimal(0.18));
    int numPeople = 4;
    //configure mock
    given(tipCalculatorMock.calcTip(billTotal, service)).willReturn(tip); (1)

    //when - call method under test
    BigDecimal shareResult = billCalculator.calcShares(billTotal, service, numPeople);

    //then - tip calculator should be called once to get result
    then(tipCalculatorMock).should(times(1)).calcTip(billTotal,service); (2)

    //verify correct result
    BigDecimal expectedShare = billTotal.add(tip).divide(new BigDecimal(numPeople));
    and.then(shareResult).isEqualTo(expectedShare);
}
1configuring response behavior of Mock
2optionally inspecting subject calls made

@InjectMocks

  • leverage Mockito to instantiate our subject(s) under test and inject them with mocks

  • takes over at least one job the @BeforeEach was performing

Alternative Mocked Unit Test
@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Bill CalculatorImpl")
public class BillCalculatorImplTest {
    @Mock
    TipCalculator tipCalculatorMock;
    /*
    Mockito is instantiating this implementation class for us an injecting Mocks
     */
    @InjectMocks (1)
    BillCalculatorImpl billCalculator;
1instantiates and injects out subject under test

Spring Boot Unit Integration Test Basics

  • pure unit testing can be efficiently executed without a Spring context

  • there will eventually be a time to either:

    • integrate peer components with one another (horizontal integration)

    • integrate layered components to test the stack (vertical integration)

  • not easily accomplished without a Spring context

  • whatever created outside of Spring context will be different from production

  • Spring Boot and Spring context can be brought into the test picture to integrate components and component infrastructure present in the end application

    • it will come at a performance cost

    • potentially add external resource dependencies

       — don’t look for it to replace lightweight pure unit testing alternatives

Adding Spring Boot to Testing

There are two primary things that will change with our Spring Boot integration test:

  1. define a Spring context for our test to operate using @SpringBootTest

  2. inject components we wish to use/test from the Spring context into our tests using @Autowire


I found the following article: Integration Tests with @SpringBootTest, by Tom Hombergs and his "Testing with Spring Boot" series to be quite helpful in clarifying my thoughts and originally preparing these lecture notes. The Spring Boot Testing reference web page provides detailed coverage of the test constructs that go well beyond what I am covering at this point in the course. We will pick up more of that material as we get into web and data tier topics.

@SpringBootTest

  • annotate test with @SpringBootTest

  • instantiates default Spring context based on configuration defined or found

@SpringBootTest Defines Spring Context for Test
package info.ejava.examples.app.testing.testbasics.tips;
...
import org.springframework.boot.test.context.SpringBootTest;
...
@SpringBootTest (1)
public class BillCalculatorNTest {
1using the default configuration search rules

Default @SpringBootConfiguration Class

  • looks for a class annotated with @SpringBootConfiguration at or above Java package containing test

  • we have a class in a parent directory that represents our @SpringBootApplication

  • @SpringBootApplication wraps @SpringBootConfiguration

  • that class will be used to define the Spring context for test

Example @SpringBootConfiguration Class
package info.ejava.examples.app.testing.testbasics;
...
@SpringBootApplication
// wraps => @SpringBootConfiguration
public class TestBasicsApp {
    public static void main(String...args) {
        SpringApplication.run(TestBasicsApp.class,args);
    }
}

Conditional Components

When using the @SpringBootApplication, all components normally a part of the application will be part of the test. Be sure to define auto-configuration exclusions for any production components that would need to be turned off during testing.

@Configuration
@ConditionalOnProperty(prefix="hello", name="enable", matchIfMissing="true")
public Hello quietHello() {
...
@SpringBootTest(properties = { "hello.enable=false" }) (1)
1test setting property to trigger disable of certain component(s)

Explicit Reference to @SpringBootConfiguration

  • can make explicit reference to class to use

    • if not in a standard relative directory

    • we wanted to use a custom version of application for testing

Explicit Reference to @SpringBootConfiguration Class
import info.ejava.examples.app.testing.testbasics.TestBasicsApp;
...
@SpringBootTest(classes = TestBasicsApp.class)
public class BillCalculatorNTest {

Explicit Reference to Components

Assuming the components required for test is known and a manageable number…​

Components Under Test
@Component
@RequiredArgsConstructor
public class BillCalculatorImpl implements BillCalculator {
    private final TipCalculator tipCalculator;
...

@Component
public class StandardTippingImpl implements TipCalculator {
   ...

We can explicitly reference component classes needed to be in the Spring context.

Explicitly Referencing Components Under Test
@SpringBootTest(classes = {BillCalculatorImpl.class, StandardTippingImpl.class})
public class BillCalculatorNTest {
    @Autowired
    BillCalculator billCalculator;

Active Profiles

  • prior to adding the Spring context, Spring Boot configuration and logging conventions were not being enacted

  • can now designate special profiles activated to define test environment

Example @ActiveProfiles Declaration
package info.ejava.examples.app.testing.testbasics.tips;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test") (1)
public class BillCalculatorNTest {
1activating the "test" profile for this test
Example application-test.properties
# application-test.properties (1)
logging.level.info.ejava.examples.app.testing.testbasics=DEBUG
1"test" profile setting loggers for package under test to DEBUG severity threshold

Example @SpringBootTest Unit Integration Test

Putting the pieces together, we have

  • a complete Spring context

  • BillCalculator injected into the test from the Spring context

  • TipCalculator injected into billCalculator instance from Spring context

  • a BDD natural-language, unit integration test that verifies result of bill calculator and tip calculator working together

@SpringBootTest
@ActiveProfiles("test")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("bill calculator")
public class BillCalculatorNTest {
    @Autowired
    BillCalculator billCalculator;

    @Test
    public void calc_shares_for_bill_total() {
        //given
        BigDecimal billTotal = BigDecimal.valueOf(100.0);
        ServiceQuality service = ServiceQuality.GOOD;
        BigDecimal tip = billTotal.multiply(BigDecimal.valueOf(0.18));
        int numPeople = 4;

        //when - call method under test
        BigDecimal shareResult=billCalculator.calcShares(billTotal,service,numPeople);

        //then - verify correct result
        BigDecimal expectedShare = billTotal.add(tip).divide(BigDecimal.valueOf(4));
        then(shareResult).isEqualTo(expectedShare);
    }
}

Example @SpringBootTest NTest Output

When we run our test we get the following console information printed. Note that

  • the DEBUG messages are from the BillCalculatorImpl

  • DEBUG is being printed because the "test" profile is active and the "test" profile set the severity threshold for that package to be DEBUG

  • method and line number information is also displayed because the test profile defines an expressive log event pattern

Example @SpringBootTest Unit Integration Test Output
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v3.3.2)

14:17:15.427 INFO  BillCalculatorNTest#logStarting:55  - Starting BillCalculatorNTest
14:17:15.429 DEBUG BillCalculatorNTest#logStarting:56  - Running with Spring Boot v2.2.6.RELEASE, Spring v5.2.5.RELEASE
14:17:15.430 INFO  BillCalculatorNTest#logStartupProfileInfo:655 - The following profiles are active: test
14:17:16.135 INFO  BillCalculatorNTest#logStarted:61   - Started BillCalculatorNTest in 6.155 seconds (JVM running for 8.085)
14:17:16.138 DEBUG BillCalculatorImpl#calcShares:24    - tip=$9.00, for $50.00 and GOOD service
14:17:16.142 DEBUG BillCalculatorImpl#calcShares:33    - share=$14.75 for $50.00, 4 people and GOOD service
14:17:16.143 INFO  BillHandler#run:24 - bill total $50.00, share=$14.75 for 4 people, after adding tip for GOOD service

14:17:16.679 DEBUG BillCalculatorImpl#calcShares:24    - tip=$18.00, for $100.00 and GOOD service
14:17:16.679 DEBUG BillCalculatorImpl#calcShares:33    - share=$29.50 for $100.00, 4 people and GOOD service

Alternative Test Slices

  • @SpringBootTest annotation is a general purpose test annotation that likely will work in many generic cases

  • other cases may need a specific database or other technologies available

  • Spring Boot pre-defines a set of Test Slices that can establish more specialized test environments

    • @DataJpaTest - JPA/RDBMS testing for the data tier

    • @DataMongoTest - MongoDB testing for the data tier

    • @JsonTest - JSON data validation for marshalled data

    • @RestClientTest - executing tests that perform actual HTTP calls for the web tier

We will revisit these topics as we move through the course and construct tests relative additional domains and technologies.

Mocking Spring Boot Unit Integration Tests

  • previous @SpringBootTest example instantiated complete Spring context to inject and execute test(s) against set of real components

  • we may need the Spring context — but do not need/want other components

  • this example will mock out TipCalculator

import org.springframework.boot.test.mock.mockito.MockBean;

import static org.assertj.core.api.BDDAssertions.and;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;

@SpringBootTest(classes={BillCalculatorImpl.class})//defines custom Spring context (1)
@ActiveProfiles("test")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Bill CalculatorImpl Mocked Integration")
public class BillCalculatorMockedNTest {
    @Autowired //subject under test (2)
    private BillCalculator billCalculator;

    @MockBean //will satisfy Autowired injection point within BillCalculatorImpl (3)
    private TipCalculator tipCalculatorMock;
1defining a custom context that excludes TipCalculator component(s)
2injecting BillCalculator bean under test from Spring context
3defining a mock to be injected into BillCalculatorImpl in Spring context

Example @SpringBoot/Mockito Test

  • actual test similar to earlier example when real TipCalculator injected

  • we must define mock’s behavior and optionally determine if it was called

@Test
public void calc_shares_for_people_including_tip() {
    //given - we have a bill for 4 people and tip calculator that returns tip amount
    BigDecimal billTotal = BigDecimal.valueOf(100.0);
    ServiceQuality service = ServiceQuality.GOOD;
    BigDecimal tip = billTotal.multiply(BigDecimal.valueOf(0.18));
    int numPeople = 4;
    //configure mock
    given(tipCalculatorMock.calcTip(billTotal, service)).willReturn(tip); (1)

    //when - call method under test (2)
    BigDecimal shareResult = billCalculator.calcShares(billTotal, service, numPeople);

    //then - tip calculator should be called once to get result
    then(tipCalculatorMock).should(times(1)).calcTip(billTotal,service); (3)

    //verify correct result
    BigDecimal expectedShare = billTotal.add(tip).divide(BigDecimal.valueOf(numPeople));
    and.then(shareResult).isEqualTo(expectedShare); (4)
}
1instruct the Mockito mock to return a tip result
2call method on subject under test
3verify mock was invoked N times with the value of the bill and service
4verify with AssertJ that the resulting share value was the expected share value

Maven Unit Testing Basics

  • production artifacts that are part of deployed artifact — placed in src/main (java and resources)

  • test artifacts are placed in src/test (java and resources)

|-- pom.xml
`-- src
    `-- test
        |-- java
        |   `-- info
        |       `-- ejava
        |           `-- examples
        |               `-- app
        |                   `-- testing
        |                       `-- testbasics
        |                           |-- PeopleFactory.java
        |                           |-- jupiter
        |                           |   |-- AspectJAssertionsTest.java
        |                           |   |-- AssertionsTest.java
        |                           |   |-- ExampleJUnit5Test.java
        |                           |   `-- HamcrestAssertionsTest.java
        |                           |-- mockito
        |                           |   `-- ExampleMockitoTest.java
        |                           |-- tips
        |                           |   |-- BillCalculatorContractTest.java
        |                           |   |-- BillCalculatorImplTest.java
        |                           |   |-- BillCalculatorMockedNTest.java
        |                           |   |-- BillCalculatorNTest.java
        |                           |   `-- StandardTippingCalculatorImplTest.java
        |                           `-- vintage
        |                               `-- ExampleJUnit4Test.java
        `-- resources
            |-- application-test.properties

Maven Surefire Plugin

Example Surefire Execution of All Example Unit Tests
$ mvn clean test
...
[INFO] Results:
[INFO]
[INFO] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  14.280 s

Filtering Tests

  • JUnit Jupiter @Tag annotations can categorize tests

    • "springboot" tag was added to all tests that launch the Spring context

    • "tips" tag was added to all tests that are part of the tips example

Example @Tag
import org.junit.jupiter.api.*;
...
@SpringBootTest(classes = {BillCalculatorImpl.class}) //defining custom Spring context
@Tag("springboot") @Tag("tips") (1)
...
public class BillCalculatorMockedNTest {
1test case has been tagged with JUnit "springboot" and "tips" tag values

Filtering Tests Executed

  • can use tag names in "groups" property specification to Maven Surefire

    • only run matching tests

    • example: all tests tagged with "tips" but not tagged with "springboot"

  • fewer tests executed and a much faster completion time

$ mvn clean test -Dgroups='tips & !springboot' -Pbdd (1) (2)
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running Bill Calculator Contract
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.41 s - in Bill Calculator Contract
[INFO] Running Bill CalculatorImpl
15:43:47.605 [main] DEBUG info.ejava.examples.app.testing.testbasics.tips.BillCalculatorImpl - tip=$50.00, for $100.00 and GOOD service
15:43:47.608 [main] DEBUG info.ejava.examples.app.testing.testbasics.tips.BillCalculatorImpl - share=$37.50 for $100.00, 4 people and GOOD service
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.165 s - in Bill CalculatorImpl
[INFO] Running Standard Tipping Calculator
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s - in Standard Tipping Calculator
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.537 s
1execute tests with tag "tips" and without tag "springboot"
2activating "bdd" profile that configures Surefire reports within the example Maven environment setup to understand display names

Maven Failsafe Plugin

  • Maven Failsafe plugin looks for classes compiled from src/test/java tree that having prefix of "IT" or suffix of "IT", or "ITCase" by default

  • Failsafe is part of the standard Maven "jar" profile

    • runs later in the build process

  • unlike Surefire that runs within one Maven phase (test) — Failsafe runs within the scope of four Maven phases:

    • pre-integration-test - when external resources get started (e.g., web server)

    • integration-test - when tests are executed

    • post-integration-test - when external resources are stopped/cleaned up (e.g., shutdown web server)

    • verify - when results of tests are evaluated and build potentially fails

Failsafe Overhead

  • external processes are normally started and stopped using Maven plugins

  • multiple phases are required for IT tests so that:

    • all resources are ready to test once the tests begin

    • all resources can be shutdown prior to failing the build for a failed test

  • With the robust capability to stand up a Spring context within a single JVM, we really have limited use for Failsafe for testing Spring Boot applications

    • exception for when we truly need to interface with something external — like stand up a real database or host endpoints in Docker images

  • Maven "integration tests" come with extra hooks and overhead that may not be technically needed for integration tests that can be executed within a single JVM

@TestConfiguration

Tests often require additional components that are not part of the Spring context under test — or need to override one or more of those components. SpringBoot supplies a @TestConfiguration annotation that:

  • allows the class to be skipped by standard component scan

  • is loaded into a @SpringBootTest to add or replace components

Example Spring Context

  • TipCalculator component located using component scan

  • has name "standardTippingImpl" if we do not supply override within @Component annotation.

Example standardTippingImpl Bean
@Primary (1)
@Component
public class StandardTippingImpl implements TipCalculator {
1declaring type as primary to make example more significant


  • bean injected into BillCalculatorImpl.tipCalculator because it implements required type

Example Injection Target for Bean
@Component
@RequiredArgsConstructor
public class BillCalculatorImpl implements BillCalculator {
    private final TipCalculator tipCalculator;

Test TippingCalculator

Our intent here is to manually write a stub and have it replace the TipCalculator from the application’s Spring context.

import org.springframework.boot.test.context.TestConfiguration;
...

@TestConfiguration(proxyBeanMethods = false) //skipped in component scan -- manually included (1)
public class MyTestConfiguration {
    @Bean
    public TipCalculator standardTippingImpl() { (2)
        return new TipCalculator() {
            @Override
            public BigDecimal calcTip(BigDecimal amount, ServiceQuality serviceQuality) {
                return BigDecimal.ZERO; (3)
            }
        };
    }
}
1@TestConfiguration annotation prevents class from being picked up in normal component scan
2standardTippingImpl name matches existing component
3test-specific custom response

Enable Component Replacement

Since we are going to replace an existing component, we need to enable bean overrides using the following property definition.

Enable Bean Override
@SpringBootTest(
        properties = "spring.main.allow-bean-definition-overriding=true"
)
public class TestConfigurationNTest {


Otherwise, we end up with the following error when we make our follow-on changes.

Bean Override Error Message
***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'standardTippingImpl', defined in class path resource
[.../testconfiguration/MyTestConfiguration.class], could not be registered.
A bean with that name has already been defined in file
[.../tips/StandardTippingImpl.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true

Embedded TestConfiguration

We can have the @TestConfiguration class automatically found using an embedded static class.

Embedded TestConfiguration
@SpringBootTest(properties={"..."})
public class TestConfigurationNTest {
    @Autowired
    BillCalculator billCalculator; (1)

    @TestConfiguration(proxyBeanMethods = false)
    static class MyEmbeddedTestConfiguration { (2)
        @Bean
        public TipCalculator standardTippingImpl() { ... }
    }
1injected billCaculator will be injected with @Bean from @TestConfiguration
2embedded static class used automatically

External TestConfiguration

Alternatively, we can place the configuration in a separate/stand-alone class.

@TestConfiguration(proxyBeanMethods = false)
public class MyTestConfiguration {
    @Bean
    public TipCalculator tipCalculator() {
        return new TipCalculator() {
            @Override
            public BigDecimal calcTip(BigDecimal amount, ServiceQuality serviceQuality) {
                return BigDecimal.ZERO;
            }
        };
    }
}

Using External Configuration

The external @TestConfiguration will only be used if specifically named in either:

  • @SpringBootTest.classes

  • @ContextConfiguration.classes

  • @Import.value

Pick one way.

Imported TestConfiguration
@SpringBootTest(
        classes=MyTestConfiguration.class, //way1 (1)
        properties = "spring.main.allow-bean-definition-overriding=true"
)
@ContextConfiguration(classes=MyTestConfiguration.class) //way2 (2)
@Import(MyTestConfiguration.class) //way3 (3)
public class TestConfigurationNTest {
1way1 leverages the `@SpringBootTest configuration
2way2 pre-dates @SpringBootTest
3way3 pre-dates @SpringBootTest and is a standard way to import a configuration definition from one class to another

TestConfiguration Result

Running the following test results in:

  • a single TipCalculator registered in the list because each considered have the same name and overriding is enabled

  • the TipCalculator used is one of the @TestConfiguration-supplied components

TipCalculator Replaced by @TestConfiguration-supplied Component
@SpringBootTest(
        classes=MyTestConfiguration.class,
        properties = "spring.main.allow-bean-definition-overriding=true")
public class TestConfigurationNTest {
    @Autowired
    BillCalculator billCalculator;
    @Autowired
    List<TipCalculator> tipCalculators;

    @Test
    void calc_has_been_replaced() {
        //then
        then(tipCalculators).as("too many topCalculators").hasSize(1);
        then(tipCalculators.get(0).getClass()).hasAnnotation(TestConfiguration.class); (1)
    }
1@Primary TipCalculator bean replaced by our @TestConfiguration-supplied bean

Summary

In this module we:

  • learned the importance of testing

  • introduced some of the testing capabilities of libraries integrated into spring-boot-starter-test

  • went thru an overview of JUnit Vintage and Jupiter test constructs

  • stressed the significance of using assertions in testing and the value in making them based on natural-language to make them easy to understand

  • introduced how to inject a mock into a subject under test

  • demonstrated how to define a mock for testing a particular scenario

  • demonstrated how to inspect calls made to the mock during testing of a subject

  • discovered how to switch default Mockito and AssertJ methods to match Business-Driven Development (BDD) acceptance test keywords

  • implemented unit integration tests with Spring context using @SpringBootTest

  • implemented mocks into the Spring context of a unit integration test

  • ran tests using Maven Surefire

  • implemented a @TestConfiguration with component override