Welcome

Welcome to the Test training for Acme

Structure

Time
  • 09:00 - 16:30? (approximately)

  • Break 45 minutes?

  • Means: 3 + 3.5 hours = 6.5 hours = 13 halve hours - 2 for slip = 11 points

  • Means that we cover a lot regarding testing which is planned during previous setup-meetings

Assumptions
  • Basic knowledge of Java development

  • Basic knowledge of Maven

  • Basic knowledge of Spring Boot

(If not explain some)

Release Notes

v1.1.0 (16-06-2022)
  • Improve the Spring Boot section after they changed their starter page

  • Remove more ing names

  • Alter companyName to Acme

  • More imporovmements

  • Tip for Intellij (Reimport). Clearer sentence regarding field Calculator calculator. Change Holder to Client

v1.0.0
  • Everything is new here! :-)

Introduction

Introduce yourself
  • Who are you

  • What is your target of this day

  • When is this day successful for you

Introduce structure
  • Topics - see TOC

  • Video - will be recorded during sessions and distributed later

  • Sourcecode for referall on Github - will be available later

Topic: Concepts

What is a good test

What’s in a good test failure bug report?
  • What were you testing?

  • What should it do?

  • What was the output (actual behavior)?

  • What was the expected output (expected behavior)?

Why testing

Testing shows the presence of errors, not the absence

— E. W. Dijkstra [1930-2002]
Pretty obvious

Software Testing is necessary because we all make mistakes. Some of those mistakes are unimportant, but some of them are expensive or dangerous. We need to check everything and anything we produce because things can always go wrong – humans make mistakes all the time

Balance

There is always a balance in testing, is a term coined by some managers. You cannot test everything since all testing will cost a lot. Personally I feel different on this. Testing should be done for all since everything we do not test will fail or the consumer will test it!

Tips from Pragmatic Programmer

Dead programs tell no lies

Which means as much that you should stop the program when something bad happens instead of letting that error getting his own life. Hence, use exceptions well and Crash Early

Assertive programming

Which means as much as that If it can’t happen, use assertions to ensure that it won’t

Leave assertions turned on, even in production although it might lead to a lot of discussion. Let’s say there are two schools about that. One says if assertions are failing in development that should surely fail in production. Other says: we should disable assertions since there is a separate error handling in production.
Use exceptions in exceptional situations

Which means as much as - as a programmer - do not overuse exceptions. Having a 0(zero) passed in to a functions which divides let’s say an int over that 0 should not throw an exception but should be handled in an other / previously way.

Code that’s easy to test

Which means as musch as to write code which is testable. Sometimes even removing some security to test it. Although that also might lead to discussion. By changing a private method in Java to default access it is testable using a Unittest in the same package (although in src/test/java folder). The win of being able to test that method / unit is greater than the loss of security.

Testing against contract

Use some contract up front to test (kind a acceptance-criteria in Scrum)

Design to test

When you design a module, or even a single routine, you should design both it’s contact and the code to test that contract.

Test your software, or your users will

Which means as much as: all software you write will be tested - if not by you and your team, then by the eventual users - so you might as wel plan on tesing it thoroughly. Test your software before those small little fish become big sharks

Don’t use manual procedures

Which means as much as to use a build system like Maven, Gradle which executes the unittests per build

Ruthless testing

Test early, test often, test automatically and coding ain’t done tll all the tests run (successfully)

Subtotal points: 1

Test pyramid

test pyramid 1

Topic: Configuration / Validation

Install
  • Java Oracle JDK (11+)

  • IntelliJ / Eclipse

  • Maven (might be in IntelliJ)

Topic: Introducing Spring Boot

The trainer will introduce Spring Boot since we are going to work with it today. To have a clue is enough for this topic

Demo and (later) Assignment: Create an empty Spring Boot project

spring boot starter II
Set properties
  • Group: nl.acme

  • Artifact: testing

  • Name: Testing Training

  • Description: This is for a description

  • Package name: nl.acme.testing

  • Packaging: jar

  • Java version: 11

Click See all

Add dependencies below
  • Core

    • Devtools

  • Web

    • Web

    • Rest Repositories

  • Rest Docs

  • SQL

    • JPA

    • MySQL

    • H2

  • Ops

    • Actuator

If you omit or forget one, no problem, we can fix that later!

Below at the button click

update dependencies

to update the dependencies and go back to the main page


Below at the button click

spring init button

to download the zip file


Move the download zip file to a convenient location

Convenient means handy to reach: so /Users/<username>/project is OK but C:\Program Files\Maven\bin\myprojects\v3.1.2\testing would be unwise :-)

Unzip the file at that location

Enter the created directory!

Run
$ mvn clean validate

⇒ Should print BUILD SUCCESS

Run
$ mvn clean package

⇒ Should print BUILD SUCCESS and should result in a .jar file in the target subdirectory

Subtotal points: 3

Topic: Unittesting

Definitions

In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use

It is imperative to remind that you see that whole as a unit. Phrased otherwise, you cannot split the test in half because than there would be nothing to test anymore. Hence, a unittest is atomic. It is not perse one method but mostly you see in Java language a unittest for one method. (but a get and a set can also be tested by a unittest, since we see that get and set as a unit)

test driven lasse koskela cover
Figure 1. Great book regarding TDD (and more)
Setting up a test following TDD

Below you find an example

BasicCalculator
package nl.ing.testing.tools;

public class BasicCalculator {

    public int add(int n, int m) {
        return n+m;
    }

    public int subtract(int n, int m) {
        return n-m;
    }

    public int multiply(int n, int m) {
        return n*m;
    }

    public double divide(int n, int m) {
        return (double) n / (double) m;
    }
}
BasicCalculatorTest
package nl.ing.testing.tools;


import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class BasicCalculatorTest {

    private BasicCalculator basicCalculator;


    @BeforeEach
    public void setUp() {
        this.basicCalculator = new BasicCalculator();
    }

    @Test
    public void testAdd() {
        Assertions.assertEquals(7, this.basicCalculator.add(3,4));
    }

    @Test
    public void testSubtract() {
        Assertions.assertEquals(3, this.basicCalculator.subtract(7,4));
    }

    @Test
    public void testMultiply() {
        Assertions.assertEquals(15, this.basicCalculator.multiply(3,5));
    }

    @Test
    public void testDivide() {
        Assertions.assertEquals(0.6, this.basicCalculator.divide(3,5), 0.1);
    }
}
Sometimes IntelliJ gets confused. Sometimes missing libraries as JUnit and so.
A possible fix is to do the following in Intellij:
  • View / Tool windows / Maven

  • In the upper left of that window click the Reimport button

Assignment: Create your first unittest

Given
  • The code above

When
  • I have a basic calculator

  • And I invoke the add method

Then
  • The result should be mathematically correct

Subtotal points: 4

Topic: Mocking

Explain here what and why Mocking
  • Doubles

    • Stub

    • Mock

Topic: Mocking with Mockito

Use the slides and ASSIGNMENT from this Mocking with Mockito page

During and After this assignment you should validate for yourself that you know that we have Mocked out the Calculator interface which is NOT THE SAME as the BasicCalculator in the Assignment on Unittesting!

Subtotal points: 6

Topic: Coverage

Introduction

During this section we will learn why, when, what and how to implement and use coverage

Why Coverage

Why Coverage? To see and show (to management mostly) that we have pretty good insight about the real quality of our code

What is Coverage

Coverage is the amount (expressed in percentages) of code which is covered during some test. We are talking about production-code lines (in src/main/java) here and not (src/test/java) So in fact …​ every test should run some productioncode. That amount is expressed as Code Coverage

How to calculate Coverage

Using an IDE

Eclipse
  • Can show coverage when you add the EclEmma plugin

IntelliJ
  • Shows coverage if you install the coverage plugin

Using a Maven reporting tool

cobertura
$ mvn clean cobertura:cobertura
Cobertura is pretty unusable since it is onlu usable up to Java 7 (Lambda expressions fail)

We will use Jacoco ⇒

Using Jacoco

Modify pom.xml (add to build/plugins section)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.2</version>
  <executions>
    <execution>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
  </executions>
</plugin>
Run build to create a jacoco.exec file (data)
$ mvn clean test
Create report in target/site/jacoco
$ mvn jacoco:report
Assignment
  • Based on the code above

  • Add jacoco to your Maven configuration

  • Run the test goal

  • Create a report

Subtotal points: 8

Bonus Assignment: Add the minimum coverage percentage

Add check goal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<execution>
  <id>jacoco-check</id>
  <goals>
    <goal>check</goal>
  </goals>
  <configuration>
    <rules>
      <rule>
        <!-- element can also be PACKAGE or LINES -->
        <element>BUNDLE</element>
        <limits>
          <limit>
            <counter>LINE</counter>
            <value>COVEREDRATIO</value>
            <minimum>0.50</minimum>
          </limit>
        </limits>
      </rule>
    </rules>
  </configuration>
</execution>
Run this
$ mvn clean verify  # the verify phase is the Maven phase in which the jacoco:check goal is run

Based on the code above, please implement this to your build

Topic: Integrationtesting

Explain what integrationtesting is

Topic: Spring Boot integrationtesting

Topic: Spring Boot short introduced

These topics will be shortly explained by the trainer

Principles and topics short introduced
  • application.properties

  • @Component

  • @Autowired

  • @Bean

Annotations needed for creating Spring Boot integration tests
  • @ActiveProfiles("name of your current profile since we are running integrationtests")

    • e.g. @ActiveProfiles("integrationtest") ⇒ Spring boot will read the application-integrationtest.properties after reading the application.properties file

  • @SpringBootTest(…​)

    • @SpringBootTest(classes=YourSpringBootApplication.class, webEnvironment=WebEnvironment…​)

    • Tells Spring Boot that on location YourSpringBootApplication.class the wiring should begin

Naming conventions of Spring / Maven
  • Unittest have filename with postfix Test.java

  • IntegrationTests have filename with postfix IT.java

  • We should use the Maven failsafe plugin to run the integrationtests

    • which are run during the verify phase of the Maven build

Spring Boot adaptations
  • Rename the test in src/test/java which are created by default from …​Test to …​IT (which means, that test is now an integrationtest, which in fact it already was)

    • In fact you might call that a bug from the Spring Boot generator

  • Add the Maven failsafe plugin to the build section of pom.xml

    • Which results in the fact that the IT files are seen as integration tests by Maven

      • That plugin might already be in, but still you have to know that he is there

      • Testable using: $ mvn dependency:tree (enter)

  • Change the first version of your sofware from 0.0.1-SNAPSHOT to 0.1.0-SNAPSHOT

Spring Boot integrationtest Demo

Flow (code will be shown below)
  • Add H2 depencency to pom.xml

  • We will create a class BankAccount - the model

  • We will create a class BankAccountService - the service with business logic

  • We will create a class BankAccountRepository - the interface which saves the data to DB

  • We will create an application-integrationtest.properties file to src/test/resources/

    • Which is used to persist to an in-memory db instead of real (MySQL) data

  • We will create a BankAccountServiceIT integration test

    • Which (integration) tests the full path from service to repo to DB

Add the H2 dependency to your pom.xml
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>test</scope>
</dependency>
BankAccount.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package nl.ing.testing.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class BankAccount {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String bankAccountNumber;
    private double saldo;
    private boolean savingsAccount;

    public boolean isSavingsAccount() {
        return savingsAccount;
    }

    public void setSavingsAccount(boolean savingsAccount) {
        this.savingsAccount = savingsAccount;
    }

    public long getId() {
        return id;
    }

    public String getBankAccountNumber() {
        return bankAccountNumber;
    }

    public double getSaldo() {
        return saldo;
    }

    public void setBankAccountNumber(String bankAccountNumber) {
        this.bankAccountNumber = bankAccountNumber;
    }

    public void setSaldo(double saldo) {
        this.saldo = saldo;
    }
}
BankAccountService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package nl.ing.testing.service;

import nl.ing.testing.model.BankAccount;
import nl.ing.testing.persistence.BankAccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Optional;

@Service
@Transactional
public class BankAccountService {

    @Autowired
    private BankAccountRepository repository;

    public BankAccount insert(BankAccount newBankAccount) {
        return this.repository.save(newBankAccount);
    }

    public Optional<BankAccount> findById(long id) {
        return this.repository.findById(id);
    }

    public BankAccount update(long id, BankAccount data) {
        BankAccount target = this.repository.findById(id).get();
        target.setBankAccountNumber(data.getBankAccountNumber());
        target.setSaldo(data.getSaldo());

        return this.repository.save(target);
    }

    public boolean deleteById(long id){
        if(this.repository.findById(id).isPresent()) {
            this.repository.deleteById(id);

            return true;
        }
        else {
            return false;
        }
    }

    public long count() {

        return this.repository.count();
    }

    public void setAllBankAccountsToZeroes() {

        this.repository.setAllBankAccountsToZeroes();
    }

    public Iterable<BankAccount> findAll() {

        return this.repository.findAll();
    }

    public void setAllBankAccountsToSavingsAccount() {
        this.repository.setAllBankAccountsToSavingsAccount();
    }
}
BankAccountRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package nl.ing.testing.persistence;

import nl.ing.testing.model.BankAccount;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BankAccountRepository  extends CrudRepository <BankAccount, Long> {

    @Query("update BankAccount set saldo='0'")
    @Modifying
    public void setAllBankAccountsToZeroes();

    @Query("update BankAccount set savingsAccount=true")
    @Modifying
    public void setAllBankAccountsToSavingsAccount();
}
src/test/resources/application-integrationtest.properties
1
2
3
4
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
#nog niet nodig spring.datasource.password=
spring.datasource.driverClassName=org.h2.Driver
Now we are ready to add the BankAccountServiceI(ntegretation)T(est)
BankAccountServiceIT.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package nl.ing.testing.service;

import nl.ing.testing.Application;
import nl.ing.testing.model.BankAccount;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.ActiveProfiles;

@ActiveProfiles("integrationtest")
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class BankAccountServiceIT {

   @Autowired
   private BankAccountService bankAccountService;

   @Test
   public void testCrud() {

      BankAccount nieuwe = new BankAccount();
      nieuwe.setSavingsAccount(true);
      nieuwe.setBankAccountNumber("Schaken");

      BankAccount bankAccountCreated = this.bankAccountService.insert(nieuwe);

      Assertions.assertTrue(bankAccountCreated.getId() != 0);

      long id = bankAccountCreated.getId();

      BankAccount opgehaald = this.bankAccountService.findById(id).get();  // rloman refactor

      Assertions.assertEquals(nieuwe.getBankAccountNumber(), opgehaald.getBankAccountNumber());

      opgehaald.setBankAccountNumber("10.21.33.44.555");

      BankAccount opgehaaldNaSaven = this.bankAccountService.update(id, opgehaald);

      Assertions.assertEquals("10.21.33.44.555", opgehaaldNaSaven.getBankAccountNumber());

      this.bankAccountService.deleteById(id);

      Assertions.assertFalse(this.bankAccountService.findById(id).isPresent());
   }

   @Test
   public void testDeletingNonExisting() {

      BankAccount nieuwe = new BankAccount();
      nieuwe.setSavingsAccount(true);
      nieuwe.setBankAccountNumber("Schaken");

      BankAccount bankAccountCreated = this.bankAccountService.insert(nieuwe);

      Assertions.assertTrue(bankAccountCreated.getId() != 0);

      long id = bankAccountCreated.getId();

      BankAccount opgehaald = this.bankAccountService.findById(id).get(); // rloman refactor

      Assertions.assertEquals(nieuwe.getBankAccountNumber(), opgehaald.getBankAccountNumber());

      opgehaald.setBankAccountNumber("Schaken en Dammen");

      BankAccount opgehaaldNaSaven = this.bankAccountService.update(id, opgehaald);

      Assertions.assertEquals("Schaken en Dammen", opgehaaldNaSaven.getBankAccountNumber());

      Assertions.assertTrue(this.bankAccountService.deleteById(id));

      Assertions.assertFalse(this.bankAccountService.deleteById(id));

   }

   @Test
   public void testSetAllBankAccountsToZero() {

     long init = this.bankAccountService.count();

      for (int i = 0; i < 3; i++) {
         BankAccount nieuwe = new BankAccount();
         nieuwe.setSavingsAccount(false);
         nieuwe.setBankAccountNumber(String.valueOf(Math.random()* 100_000_000));

         this.bankAccountService.insert(nieuwe);
      }

     Assertions.assertEquals(init+3, this.bankAccountService.count());

     this.bankAccountService.setAllBankAccountsToZeroes();

     this.bankAccountService.findAll().forEach(s -> {
        Assertions.assertEquals(0, s.getSaldo(), 0.01);
     });

     Assertions.assertEquals(3, this.bankAccountService.count());
   }

   @Test
   public void testSetAllBankAccountsToSavingsAccount() {

      long init = this.bankAccountService.count();

      for (int i = 0; i < 3; i++) {
         BankAccount nieuwe = new BankAccount();
         nieuwe.setSavingsAccount(false);

         this.bankAccountService.insert(nieuwe);
      }

      this.bankAccountService.setAllBankAccountsToSavingsAccount();

      this.bankAccountService.findAll().forEach(s -> {
         Assertions.assertTrue(s.isSavingsAccount());
      });

      Assertions.assertEquals(init+3, this.bankAccountService.count());
   }
}

Spring Boot integrationtest Assignment I

Based on the code above implement the BankAccountServiceIT.java

We perform this assignment to have a running integrationtest in your environment. In fact the first, mental creation. The next, the second physical creation will be the following assignment where you will learn how to make one yourself!!!

Spring Boot integrationtest Assignment II

When the BankAccountServiceIT successfully runs, now please create an integrationtest for a NEW domain class Client (Dutch: Rekeninghouder / Client) using the same principles as used for the BankAccount. Be creative and assume some properties of the Client for yourself

Subtotal points: 11

Resources