-
These are my notes while working through Chad Darby's Udemy course on unit testing with the spring framework! 😊
-
I started this course on 15/04/2023 📅
-
You can find the course here
-
Spring Boot supports unit test and integration testing vvia JUnit and Mockito
-
This course will show you how to take advantage of testing support in Spring
-
The course road map consists of:
-
JUnit Review
-
Spriong Boot Mocks with Mockito
-
Spring Boot Testing Spring MVC Web App
-
Spring Boot Testing Spring REST APIs
-
-
Dev environment will be intelliJ for Maven support, and MySQL Database Server and Workbench
-
I downloaded and installed the MySQL Developer Default setup!
-
The source code for this course can be found here
- Typically we write small programs which test our code and examine the results. The test case is not reproduceable and we are unable to automate the tests
-
🎃 Unit Testing is testing individual unit of code. We have a fixed input and expected known output 🎃
-
Suppose we had a calculator program with an
add(int x, int y)
method- We would write tests such as:
-
Does it work with positive?
-
Does it work with negatives?
-
Does it work with zero?
-
Automated tests✔️ Better Code Design✔️ Fewer bugs and reliability✔️
-
We can be confident extending/modifying our code ✔️
-
Great for automated build processes ✔️
-
🎃 Integeration Testing is testing multiple components together as part of a test plan 🎃
-
We can determine if units of code can work together ✔️
-
We can use mocks and stubbing for integration tests ✔️
-
JUnit:
- Supports creating test cases
- Automation of test cases
- Utilities for setup and tearing down tests
-
Mockito:
- Used for creating mocks and stubs
- Minimise dependencies (such as databases)
-
We shall look at some simple examples for unit testing 🎯
-
We will look at fundamentals like defining and running tests, and using assertions 🎯
-
Discuss Test-Driven Development 🎯
- We shall create a
DemoUtil
class with a method:add(int a, int b)
, and write unit tests for it. The development process is:
-
Create a Maven project with JUnit dependency 🎯
-
Create test package, write unit test and run test 🎯
- The Maven dependency is:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
-
The scope is saying that this is for testing only, not for normal execution of the code
-
We shall setup a
test
package and follow the convention of matching the package structure of themain
package -
Unit tests have the following structure:
- Setup
- Execute
- Assert
-
We use the
@Test
annotation above the test method. -
We setup by instantiating thr
DemoUtil
, execute theadd()
method and assert the expected result -
We then run the test in the IDE
-
JUnit
has a collection of assertions which are defined inorg.junit.jupiter.api.Assertions
-
The assertion methods include:
-
assertEquals(expected, actual, optionalReturnMessage)
-
assertNotEquals(unexpected, actual, optionalReturnMessage)
-
assertNull(actual, optionalReturnMessage)
-
assertNotNull(actual, optionalReturnMessage)
- We can import the methods from
Assertions
class statically, so we can call the methods without referencing the class
import static org.junit.jupiter.api.Assertions.*; // imports ALL methods
-
I download the code repository from https://github.com/darbyluv2code/spring-boot-unit-testing
-
I copy
1.00-starting-project
into this folder -
I open up the POM in intelliJ and see this:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.luv2code</groupId>
<artifactId>junitdemo</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
- I add the JUnit dependency using intelliJ's generate button:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
- I add a new package into the
test
package and theDemoUtilTest
class:
- I write the test:
@Test
void testEqualsAndNotEquals(){
// setup:
DemoUtils demoUtils = new DemoUtils();
// using assertEquals and NotEquals:
assertEquals(6, demoUtils.add(2,4), "2+4 should equal 6");
assertNotEquals(6, demoUtils.add(1,9), "1+9 should not be 6");
}
- I run the test and it passes as expected:
- I create a method for checking Nulls:
@Test
void testNullAndNotNull() {
DemoUtils demoUtils = new DemoUtils();
String str1 = null; String str2 = "luv2code";
assertNull(demoUtils.checkNull(str1));
assertNotNull(demoUtils.checkNull(str2));
}
- The test passes as expected!!!
-
Some common operations is setting up the environment and cleaning things after tests finish.
-
JUnit provides Lifecycle Method Annotations, including:
-
@BeforeEach
- Method is executed before EACH test method -
@AfterEach
- Method to be executrer after EACH test method -
@BeforeAll
- Only executed ONCE before all test methods. MUST be static -
@AfterAll
- Only executed ONCE after all test methods. MUST be static
-
I remove the instantiation of the
DemoUtils
class in each test method and setup a class field. -
I use the
@BeforeEach
annotation to write a method which instantiatesDemoUtils
, and write a method with@AfterEach
which just prints a diagnostic message:
public class DemoUtilsTest {
// Creating a DemoUtil field
DemoUtils demoUtils;
@BeforeEach
void setUpBefore(){
demoUtils = new DemoUtils();
System.out.println("BeforeEach executes before each test method");
}
@Test
void testEqualsAndNotEquals(){
System.out.println("Running test: testEqualsAndNotEquals");
// using assertEquals and NotEquals:
assertEquals(6, demoUtils.add(2,4), "2+4 should equal 6");
assertNotEquals(6, demoUtils.add(1,9), "1+9 should not be 6");
}
@Test
void testNullAndNotNull() {
System.out.println("Running test: testNullAndNotNull");
String str1 = null; String str2 = "luv2code";
assertNull(demoUtils.checkNull(str1));
assertNotNull(demoUtils.checkNull(str2));
}
@AfterEach
void tearDownAfterEach(){
System.out.println("Running @AfterEach");
}
}
- Running the test class, shows the following in console:
BeforeEach executes before each test method
Running test: testNullAndNotNull
Running @AfterEach
BeforeEach executes before each test method
Running test: testEqualsAndNotEquals
Running @AfterEach
-
The name of the methods show up in the test results
-
We can use custom display names to provided descriptive names for each tests
-
🎃 We use
@DisplayName
to make a custom display name 🎃 -
We can add this annotation below the
@Test
annotation:
@Test
@DisplayName("Null and not null")
void testNullAndNotNull(){
// test implementation ....
}
- JUnit can create display names for you:
-
Simple
- removes the () for you -
ReplaceUnderscores
- this replaces _ with spaces -
IndicativeSentences
- creates name based on test class name and method name
- We can use generators WITHOUT using
@DisplayName
for every method. We can define one main annotation at class level using@DisplayNameGeneration
(⚠️ can not use it on methods!)
@DisplayNameGeneration(DisplayNameGenerator.Simple.class)
class DemoUtilsTest {
//....
}
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class DemoUtilsTest {
// ...
}
@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
class DemoUtilsTest {
// ...
}
-
I start working on the demo here
-
Using the following annotation:
@DisplayNameGeneration(DisplayNameGenerator.Simple.class)
public class DemoUtilsTest {
// ....
}
- This displays:
- I can override the generation at class level:
@Test
@DisplayName("This tests shows equality")
void testEqualsAndNotEquals(){
System.out.println("Running test: testEqualsAndNotEquals");
// using assertEquals and NotEquals:
assertEquals(6, demoUtils.add(2,4), "2+4 should equal 6");
assertNotEquals(6, demoUtils.add(1,9), "1+9 should not be 6");
}
- This displays:
-
assertSame()
- asserts that items refer to same object -
assertNotSame()
- asserts that items do NOT refer to same object
@Test
void testSameAndNotSame() {
String str = "123";
assertSame(str, "123"); // PASSES!
assertNotSame(str, new DemoUtilsTest()); // PASSES!
}
-
assertTrue
- asserts condition is true -
assertFalse
- asserts condition is false
@Test
void testTrueAndFalse(){
assertTrue(1==1, "one must equal one!"); // PASSES!
assertFalse(1>1, "one cannot be greater than one!"); // PASSES
}
- The
assertArrayEquals()
method asserts two object arrays are deeply equal (content is the same)
@Test
void arrayIsDeeplyEqual(){
String[] firstThreeLetters = {"A", "B", "C"};
assertArrayEquals(firstThreeLetters, new String[] {"A", "B", "C"}); // PASSES!
assertArrayEquals(firstThreeLetters, new String[] {"A", "C", "B"}); // FAILS!!
}
- The
assertIterableEquals()
method asserts iterable objects are deeply equal! Classes which implement Iterable include LinkedList, ArrayList, HashSet, ...
@Test
void iterableIsDeeplyEqual(){
List<String> list = List.of("Shiv", "Likes", "Java");
assertIterableEquals(list, List.of("Shiv","Likes", "Java")); // PASSES!
assertIterableEquals(list, List.of("Shiv","Likes")); // FAILS!
}
- The
assertLinesMatch
asserts that list of Strings match!
@Test
void LinesShouldMatch(){
List<String> list = List.of("Shiv", "Likes", "Java");
assertLinesMatch(list, List.of("Shiv","Likes","Java")); // PASSES!!
assertLinesMatch(list, List.of("Shiv","Java","Likes")); // FAILS!!
}
-
assertThrows(Class expectedType, Executable executable)
- asserts if an exception is throw in a given lambda -
assertDoesNotThrow(Executable executable)
- asserts that ANY exception is not thrown -
The
DemoUtils
class has the following method:
public class DemoUtils {
public String throwException(int a) throws Exception {
if (a<0)
throw new Exception("Value should be greater than or equal to 0");
return "Value is greater than or equal to 0";
}
}
- We can write a test:
@Test
void testThrowsAndDoesNotThrow() {
assertThrows(Exception.class,
()->{ demoUtils.throwException(-1); },
"An exception should be thrown"); // PASSES!!
assertThrows(Exception.class,
()-> {throw new RuntimeException();},
"Should throw Exception class"); // PASSES
assertThrows(RuntimeException.class,
()-> { throw new Exception();},
"Should throw RuntimeException class"); // FAILS
assertDoesNotThrow(()-> {demoUtils.throwException(1);} ); // PASSES!!
}
-
assertTimeOut(Duration stimeout, Executable executable)
- asserts that execution completes before time out is exeeded -
assertTimeoutPreemptively(Duration timeout, Executable executable)
- again, asserts execution completes before timeout is exceeded, but also aborts execution after timeout -
The
DemoUtils
class has the following method:
public class DemoUtils {
public void checkTimeOut() throws InterruptedException {
System.out.println("I am going to sleep");
Thread.sleep(2000);
System.out.println("Sleeping over");
}
}
- I write the following test:
@Test
void testTimeOutAndTimeOutPremptively(){
assertTimeout(Duration.ofSeconds(3), ()-> {demoUtils.checkTimeout(); } ); // PASSES!
/* CONSOLE:
I am going to sleep
Sleeping over
*/
assertTimeout(Duration.ofSeconds(2), ()-> {demoUtils.checkTimeout(); } ); // FAILS!
/* CONSOLE:
I am going to sleep
Sleeping over
org.opentest4j.AssertionFailedError: execution exceeded timeout of 2000 ms by 6 ms
*/
assertTimeoutPreemptively(Duration.ofSeconds(1), ()->{demoUtils.checkTimeout();} ); // FAILS!
/* CONSOLE
I am going to sleep
org.opentest4j.AssertionFailedError: execution timed out after 1000 ms
*/
}
- We can visually change the order of the test report in the IDE. Without any ordering the test results looks like:
-
By default, the tests are deterministic but sorted in a pseudo random way intentionally.
-
⚠️ In general, ordering should not actually be part of the tests. Unit testing is meant to be independent!⚠️
-
We can use the
@TestMethodOrder()
annotation to specify ordering. -
We have 4 ways of ordering:
-
MethodOrderer.DisplayName
sorts test methods alphanumerically using display names -
MethodOrderer.MethodName
sorts tests methods alphanumerically using method names -
MethodOrderer.Random
sorts test methods randomly! -
MethodOrderer.OrderAnnotation
sorts method based on@Order
annotation
-
The
@Order
annotation lets us specify priority of a test using anint
-
E.g., if we have the following ordering:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayNameGeneration(DisplayNameGenerator.Simple.class)
public class DemoUtilsTest {
DemoUtils demoUtils= new DemoUtils();
@Order(1)
@Test
@DisplayName("This tests shows equality")
void testEqualsAndNotEquals(){
// ....
}
@Order(-33)
@Test
void testNullAndNotNull() {
// ....
}
// other tests with no annotation
}
- Then the test results look like:
-
💡 The rest of the tests follow the deterministic algorithmic ordering 💡
-
💡 If there are duplicate entries, then it will be ordered using the algorithm forementioned 💡
-
💡 Code coverage measures how many of the methods and lines are called via your test 💡
-
🎃 We should strive for 70-80% code coverage, 100% code coverage can be achieved but would not be a meaningful metric 🎃
-
IntelliJ has built in support for code coverage! We can also generate reports using Maven in command line
-
In order to see the report in intelliJ, you right-click and go to:
-
We can see the code coverage in the
DemoUtils
class:- We can see the red lines means not covered and green means code is covered
-
We can generate a report by clicking on:
-
We then get a promp for where to store the report:
-
If we open up the report, and navigate to the class:
- We can see the
muliply()
method has not been covered, so we could write a test for it
- We can see the
-
If we want to generate a test report, we can click on:
-
Opening the report shows:
-
We can also do Code coverage using Maven in the command line!
-
You must have Maven installed (download here) - make sure to download the binary!
- 💡 By default Maven will NOT find JUnit 5 tests, we must configure it! We can add Maven Surefire plugin version 2.22.0 or higher. I add the following to the POM:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifaceId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</build>
-
I navigated to my projects source folder (here)
-
I ran
mvn clean test
and the test ran in the command prompt!:
- We can use the SureFire plugin to generate unit test reports! We need to add some information to the
<plugin>
tag:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
-
This saying, during "test" phase execute the "report" goal
-
Re-running
mvn clean test
-
The report gets generated here:
- By default, Surefire will not generate reports if test fails. We can add a configuration to the plugin in the POM xml:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.0.0-M5</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
-
If we run
mvn clean test
then we get reports with no stylings: -
We need to execute another command, to allow for the css and images:
mvn site -DgenerateRerports=false
-
Our report is now styled:
-
We need to enable the report to be generated WHEN tests FAIL! We need to add a configuration:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.0.0-M5</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
- I'll add a failing test and rerun
mvn clean install
andmvn site -DgenerateReports=false
@Test
void purposlyFailingTest(){
assertTrue(false);
}
-
And the report generates🎉:
-
We also need to configure the reports to show the Display names!!! I search the maven repo wiki and find: this:
-
I update the POM:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
<statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<usePhrasedFileName>false</usePhrasedFileName>
<usePhrasedTestSuiteClassName>true</usePhrasedTestSuiteClassName>
<usePhrasedTestCaseClassName>true</usePhrasedTestCaseClassName>
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
</statelessTestsetReporter>
<consoleOutputReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5ConsoleOutputReporter">
<disable>false</disable>
<encoding>UTF-8</encoding>
<usePhrasedFileName>false</usePhrasedFileName>
</consoleOutputReporter>
<statelessTestsetInfoReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoReporter">
<disable>false</disable>
<usePhrasedFileName>false</usePhrasedFileName>
<usePhrasedClassNameInRunning>true</usePhrasedClassNameInRunning>
<usePhrasedClassNameInTestCaseSummary>true</usePhrasedClassNameInTestCaseSummary>
</statelessTestsetInfoReporter>
</configuration>
</plugin>
- We need another pluging:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<id>jacoco-prepare</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
- We may want to disable a given test, or have a test which only runs on specific versions of Java, or or run on a specific OS.
-
@Disabled
- the test will not be executed but STILL show in the test reports. -
@EnabledOnOs
- we can specify an OS, or multiple OS's in an array
@Test
@Disabled("Do not run till issue fixed")
void someIgnoredTest() { /* ... */}
@Test
@EnabledOnOs(OS.MAC)
void testOnlyOnMAC() { /* ... */}
@Test
@EnabledOnOs({OS.WINDOWS, OS.MAC})
void testOnlyOnWindowsOrMAC() { /* ... */}
![](screenshots/2023-04-16-12-26-37.png)
- We can disable/enable a test for a particular JRE or range:
@Test
@EnabledOnJre(JRE.JAVA_17)
@Order(10)
void testOnlyForJRE17(){ }
@Test
@Order(11)
@EnabledForJreRange(min = JRE.JAVA_17)
void testForAnythingAbove16() { }
@Test
@Order(12)
@DisabledForJreRange(max = JRE.JAVA_17)
void testDisabledForAnyJREBelow18(){} // DOES NOT RUN
- We can enable/disable tests depending on system properties or environment variable
@Test
@Order(13)
@EnabledIfEnvironmentVariable(named = "MAVEN_HOME", matches = "C:\\\\apache-maven-3.6.3")
void testShouldBeExecuted(){}
@Test
@Order(14)
@EnabledIfEnvironmentVariable(named = "MAVEN_HOME", matches = "hello world")
void testShouldNotBeExecuted() {}
-
In the past, development would have the following flow: Design ➡️ Code ➡️ Test.
-
TDD has the following cycle: Test ➡️ Code ➡️ Refactor 🔁
-
TDD is beneficial as it outlines the things to test and develop, identify edge cases, improve confidence in code base and the ability to refactor code!
-
Write a program which prints the first 100 FizzBuss numbers, if the number is
- divisible by 3 then print Fizz
- divisible by 5 then print Buzz
- divisible by 3 and 5 then print FizzBuzz
- number is not divisible by 3 or 5 then print the number
public class FizzBuzzTest {
@Test
void testForDivisibleByThree(){
fail("fail");
}
}
- I update my test:
public class FizzBuzzTest {
@Test
void testForDivisibleByThree(){
assertEquals("Fizz", FizzBuzz.compute(3), "3 should return Fizz");
}
}
- I create my FizzBuzz class in the src package:
public class FizzBuzz {
}
- I create the
compute()
method so the test passes!
public class FizzBuzz {
public static String compute(int i) {
return null;
}
}
public static String compute(int i) {
if (i % 3 == 0)
return "Fizz";
return null;
}
- I create a new test:
@Test
void testForDivisubleByFive(){
assertEquals("Buzz", FizzBuzz.compute(5), "5 should return Buzz");
}
- The test fails as expected! So I update the
compute()
method:
public static String compute(int i) {
if (i % 3 == 0)
return "Fizz";
else if (i % 5 ==0) {
return "Buzz";
}
return null;
}
- All tests now pass! I now write a test for 15 which should return FizzBuzz:
@Test
void testForDivisbleBy3And5(){
assertEquals("FizzBuzz", FizzBuzz.compute(15), "15 should return FizzBuzz");
}
- The test fails because Fizz returns! I update the
compute()
method:
public static String compute(int i) {
if (i % 3 == 0 && i % 5 ==0)
return "FizzBuzz";
else if (i % 3 == 0)
return "Fizz";
else if (i % 5 ==0) {
return "Buzz";
}
return null;
}
- I add a new test for a number which is neither Fizz or Buzz:
@Test
void testForNonFizzBuzzNumber(){
assertEquals("1", FizzBuzz.compute(1), "1 should return 1");
}
- It fails because it returns null! I update the
compute()
method again:
public static String compute(int i) {
if (i % 3 == 0 && i % 5 ==0)
return "FizzBuzz";
else if (i % 3 == 0)
return "Fizz";
else if (i % 5 ==0) {
return "Buzz";
}
return ""+i;
}
- All tests pass!
- Since we have all our test passing, we can now refactor our method!
public class FizzBuzz {
public static String compute(int i){
StringBuilder sb = new StringBuilder();
if (i%3 == 0)
sb.append("Fizz");
if (i%5 == 0)
sb.append("Buzz");
if (sb.isEmpty())
sb.append(i);
}
}
- All tests still pass!
-
Suppose we want to pass a collection of inputs and expected results for our FizzBuzz program
-
One way could be to use an array:
void testLoopOverArray(){
String[][] data = { { "1", "1"},
{ "2", "2"},
{ "3", "Fizz"},
{ "4", "4"},
{ "5", "Buzz"},
{ "6", "Fizz"},
{ "7", "7"},
};
for (int i=0; i<data.length; i++){
String actual = data[i][0];
String expected = data[i][1];
assertEquals(expected, FizzBuzz.compute(Integer.parseInt(value)));
}
}
-
😱 JUnit provides
@ParameterizedTest
which lets you run a test multiple times with different parameters. It does the looping for you in the background😮💨😮💨😮💨 -
We can provide the parameters using an array of values✅, array of CSV Strings✅, CSV values from a file✅, an enum✅ or even a custom method✅
Annotation | Description |
---|---|
@ValueSource |
Arrays of values |
@CsvFileSource |
CSV values read from a file |
@CscSource |
CSV values from a String |
@EnumSource |
Enum constant values |
@MethodSource |
Custom method for providing values |
@ParameterizedTest
@CsvSource({"1,1",
"2,2",
"3,Fizz",
"4,4",
"5,Buzz",
"6,Fizz",
"15,FizzBuzz"})
void testUsingParameters(int number, String result){
assertEquals(FizzBuzz.compute(number), result);
}
@ParameterizedTest(name = "value={0}, expected={1}")
@CsvFileSource(resources = "/small-test-data.csv")
void test(int value, String expected){
assertEquals(FizzBuzz.compute(value), expected);
}
-
I specified a name on the @ParameterizedTest annotation to override the display name for each test!
-
I copy
large-test-data.csv
andmedium-test-data.csv
into my resources: -
I create a new method to test the large CSV:
@ParameterizedTest(name="value:{0}, expected:{1}")
@CsvFileSource(resources = "/large-test-data.csv")
void testWithLargeData(int value, String expected){
assertEquals(FizzBuzz.compute(value), expected);
}
-
The initial task was to write the numbers from 1 to 100 using FizzBuzz!
-
I define a
MainApp
class in the tdd package in main
public class MainApp {
public static void main(String[] args) {
for (int i=1;i<=100;i++){
System.out.println(FizzBuzz.compute(i));
}
}
}
- The app works as expected!!!
-
You need the following for unit testing Spring Boot applications: Access to Spring Application Context✅, support for dependency injection✅, Retrieve data from Spring application.properties✅, Mock object support for web, data, REST APIs... ✅
-
Spring Boot provided rich testing support.
@SpringBootTest
: loads application context, dependency injection support, access data from Spring application.properties
- We can add spring-boot-starter-test which includes a transitive dependency on JUnit 5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
public class ApplicationExampleTest {
@Test
void basicTest(){
// ...
}
}
- The
@SpringBootTest
loads up the application context, this opens up the oppurtunity to inject dependencies. Suppose we have aStudentGrades
component:
@Component
public class StudentGrades{
// ...
}
- We can inject the dependency using
@Autowired
:
@SpringBootTest
public class ApplicationExampleTest {
@Autowired
StudentGrades studentGrades;
@Test
void basicTest(){
// ...
}
}
- We can also access application properties. Suppose we have the following properties:
info.school.name=luv2code
info.app.name=My super cool Gradebook
- We can access the properties witin out Test class:
@SpringBootTest
public class ApplicationExampleTest {
@Value("${info.school.name}")
private String schoolName;
@Value("${info.app.name}")
private String appInfo;
@Test
void basicTest(){}
}
-
You can implicitly use the base searched package if you place your test class in test package same as your main package.
-
E.g.:
-
Suppose we wanted to place our Test class not in the default leveraged package (e.g. com.luv2code.test). We then need to make an an explicit reference to the main SpringBoot class:
@SpringBootTest(classes = MvcTestingExampleApplication.class)
public class ApplicationExampleTest {
}
- Suppose we have a student grading system which was coded by a previous employee. The code does NOT use ANY unit tests😱 We have been tasked with devloping the unit tests
-
I open 2.00-starting-project
-
We have a
Student
interface:
public interface Student {
String studentInformation();
String getFullName();
}
- A
CollegeStudent
implementation:
public class CollegeStudent implements Student {
private String firstname;
private String lastname;
private String emailAddress;
private StudentGrades studentGrades;
public CollegeStudent() {}
public CollegeStudent(String firstname, String lastname, String emailAddress) {
this.firstname = firstname;
this.lastname = lastname;
this.emailAddress = emailAddress; }
// GETTERS AND SETTERS ...
@Override
public String toString() {
return "CollegeStudent{" + "firstname='" + + '\'' ", lastname='" + lastname + '\'' + ", emailAddress='" + emailAddress + '\'' + ", studentGrades=" + studentGrades +'}';
}
@Override
public String studentInformation() {
return getFullName() + " " + getEmailAddress();
}
@Override
public String getFullName() {
return getFirstname() + " " + getLastname();
}
}
- A
StudentGrades
class:
@Component
public class StudentGrades {
List<Double> mathGradeResults;
/*
* CAN HAVE MULTIPLE DIFFERENT TYPES OF GRADES
* FOR 2.x WE WILL ONLY HAVE A MATH GRADE
* */
public StudentGrades() {
}
public StudentGrades(List<Double> mathGradeResults) {
this.mathGradeResults = mathGradeResults;
/*
Add other subject grades here in future lessons
*/
}
/*
Add other subject grades here in future lessons
*/
public double addGradeResultsForSingleClass(List<Double> grades) {
double result = 0;
for (double i : grades) {
result += i;
}
return result;
}
public double findGradePointAverage (List<Double> grades ) {
int lengthOfGrades = grades.size();
double sum = addGradeResultsForSingleClass(grades);
double result = sum / lengthOfGrades;
// add a round function
BigDecimal resultRound = BigDecimal.valueOf(result);
resultRound = resultRound.setScale(2, RoundingMode.HALF_UP);
return resultRound.doubleValue();
}
public Boolean isGradeGreater(double gradeOne, double gradeTwo) {
if (gradeOne > gradeTwo) {
return true;
}
return false;
}
public Object checkNull(Object obj) {
if ( obj != null ) {
return obj;
}
return null;
}
public List<Double> getMathGradeResults() {
return mathGradeResults;
}
public void setMathGradeResults(List<Double> mathGradeResults) {
this.mathGradeResults = mathGradeResults;
}
@Override
public String toString() {
return "StudentGrades{" +
"mathGradeResults=" + mathGradeResults +
'}';
}
}
- And the Spring Boot Application class:
@SpringBootApplication
public class MvcTestingExampleApplication {
public static void main(String[] args) {
SpringApplication.run(MvcTestingExampleApplication.class, args); }
@Bean(name = "collegeStudent")
@Scope(value = "prototype")
CollegeStudent getCollegeStudent() {
return new CollegeStudent();
}
}
-
I create a new copy of the 2.00-starter-project and place it here
-
I add the
spring-boot-starter-test
dependency into the POM:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
@SpringBootTest
public class ApplicationExampleTest{
@Test
void basicTest(){}
}
-
I change the package of the test class to
com.luv2code.component
to match consistency of the main application's package. The test passes as expected -
⚠️ IF we had issues with the test class not being able to load the configuration, THEN we can use the@SpringBootTest
annotation and explicitly specify the SpringBoot application class:⚠️
@SpringBootTest(classes = MvcTestingExampleApplication.class)
public class ApplicationExampleTest {
@Test
void basicTest(){}
}
-
We can access values from the
application.properties
file using the@Value
annotation. -
E.g. suppose we have a property labeled info.app.name, we can set the value of a field and write a unit test to verify its value:
@SpringBootTest(classes = MvcTestingExampleApplication.class)
public class ApplicationExampleTest {
private static int count = 0;
@Value("${info.app.name}")
private String appInfo;
@Test
void checkAppInfo(){
assertEquals(appInfo, "My Super Cool Gradebook");
}
}
- I update my test class so it has the properties and dependencies injected:
@SpringBootTest(classes = MvcTestingExampleApplication.class)
public class ApplicationExampleTest {
private static int count = 0;
@Value("${info.app.name}")
private String appInfo;
@Value("${info.app.description}")
private String appDescription;
@Value("${info.app.version}")
private String appVersion;
@Value("${info.school.name}")
private String schoolName;
@Autowired
CollegeStudent student;
@Autowired
StudentGrades studentGrades;
@Test
void checkAppInfo(){
assertEquals(appInfo, "My Super Cool Gradebook");
}
}
- I create a
BeforeEach
method which initialises a Student object and increments the count:
@BeforeEach
void beforeEach(){
count = count++;
System.out.println("Testing: "+appInfo + " which is "+appDescription
+ " Version: " + appVersion + "Executing Test method: "+ count);
student.setFirstname("Shiv");
student.setLastname("Kumar");
student.setEmailAddress("shiv.kumar@luv2code.com");
studentGrades.setMathGradeResults(Arrays.asList(100.0, 85.0, 76.50, 91.75));
}
- I add a new test for adding student grades:
@Test
@DisplayName("Add grade results for student grades")
void addGradeResultsForStudentGrades(){
assertEquals(353.25, studentGrades.addGradeResultsForSingleClass(studentGrades.getMathGradeResults()));
}
- Here is an assertNotEqualsTest:
@Test
@DisplayName("Add grade results for student grades not equal")
void addGradeResultsForStudentGradesAssertNotEqual(){
assertNotEquals(100, studentGrades.addGradeResultsForSingleClass(studentGrades.getMathGradeResults()));
// actual is 353.25
}
- Both tests pass as expected!
- Here is a test which tests the
isGradeGreater
@Test
@DisplayName("Is grades greater?")
void isGradeGreaterStudentGrades(){
assertTrue(studentGrades.isGradeGreater(999, 1));
// Implementation:
// if (gradeOne > gradeTwo) {
// return true;
// }
// return false;
}
- Here is a test which asserts given object is not null:
@Test
@DisplayName("Check Null for student grades")
void checkNullForGrades(){
assertNotNull(studentGrades.getMathGradeResults());
}
- We can create prototype beans like in the following test:
@DisplayName("Create student without grade init")
@Test
void createStudentWithoutGradesInit(){
// autowired ApplicationContext into this class
CollegeStudent studentTwo = context.getBean(
"collegeStudent", CollegeStudent.class
);
studentTwo.setFirstname("Shiv");
studentTwo.setLastname("Kumar");
studentTwo.setEmailAddress("shiv.kumar@luv2code.com");
assertNull(studentGrades.checkNull(studentTwo.getStudentGrades())); // passes!
}
- We can verify the autowired
student
is not the same as the prototype throughassertNotSame
@:
@DisplayName("Verify students are prototypes")
@Test
void verifyStudentArePrototypes(){
// CollegeStudent is not a service!
CollegeStudent studentTwo = context.getBean(
"collegeStudent", CollegeStudent.class
);
assertNotSame(student, studentTwo);
}
- The
assertAll
let's us asserts that all assertions hold true, we use a lambda expression with with:
@DisplayName("Using assertAll")
@Test
void usingAssertAll(){
assertAll(
() -> assertEquals( true, true),
() -> assertEquals(false,false)
);
}