Skip to main content

Integration Testing

Metadata
Lecture equivalentDuration
32h 15min

At the end of this task, students

  • have added one integration test (IT) to the spring-starter
  • have added a docker-compose.yml file
  • run the integration test in test containers
  • run a docker security scan upon every pull request

Add test

๐Ÿง‘๐Ÿฝโ€๐ŸŽ“-advice

This task combines literally everything you learned until now. Mastering this task requires time and the will to learn, there is no fast-lane. The result will be fascinating, mature and powerful. But if you expect that you can do this by clicking three times in a UI, you did not pay attention the previous 18 lessons.

As a reward for your efforts you get a fully-automated, state-of-the-art CI pipeline using GitHub Actions!

Refactor EmployeesController

In the spring-starter task we created two endpoints, one seemingly secure, the other not secure.

Task

Remove the insecure endpoint. Refactor the secure endpoint to use a layered architecture to fetch three fictional employees. Remove also the empty endpoints from architecture, you also do not need them anymore.

Merge these two tasks (spring-starter and architecture) into one /employees endpoint!

First integration test

Task

Write your first integration test as shown below.

Create a new file: EmployeeControllerTestIT.class (the IT stands for integration test and helps to distinguish files)

Use these sources to write your first own integration test (you can also cheat, learning nothing):

  1. https://rest-assured.io/
  2. https://github.com/rest-assured/rest-assured/wiki/GettingStarted#rest-assured
  3. https://github.com/rest-assured/rest-assured/wiki/Usage#challenged-basic-authentication
  4. https://github.com/rest-assured/rest-assured/wiki/Usage#root-path --> last example
  • note body("size()", equalTo(4)).

Compose these sources into one test. Try as hard as you can, before you cheat.

Run tests

First, make sure that your old EmployeeTest (from github-actions) still works. If the old test still passes, run the new test.

You will hit:

java.net.ConnectException: Connection refused

at java.base/sun.nio.ch.Net.connect0(Native Method)
at java.base/sun.nio.ch.Net.connect(Net.java:503)
at java.base/sun.nio.ch.Net.connect(Net.java:492)
...

Why?

success

Integration test test against a running instance! So you need your server running at the same time as the tests to succeed!

So, in IntelliJ (or the console), start the server, then run the tests (IntelliJ can handle that, yes).

Task

Make your tests success the first time! All of them

  • EmployeeTest.java
  • EmployeeControllerTestIT.java

In order for this to work properly, you might give a look at JUnit 5 Test Tagging.

Interim report

Congrats, you have made it quite far! Now lets automate some things away!

Automate

Create docker-compose.yml

We have seen in the lecture and you can read up here what a docker-compose.yml file does.

Task

Add a docker compose definition to your project. You can either fill it in yourself or see its content here.

This compose file will be used by a gradle plugin to up and down your project automatically during test phase! No more manually starting the project!

Separate unit and integration tests

Modify your gradle test task to look like so:

test {
useJUnitPlatform{
includeTags 'unit'
excludeTags 'IT'
}
testLogging {
events "passed", "skipped", "failed"
}
}

This will only run unit tests when test is called. Now it is time to modify our gradle file further, add the following content (there is already a plugins section, merge them):

plugins {
id 'com.avast.gradle.docker-compose' version "0.14.3"
...
}

task integrationTest(type: Test) {
dependsOn assemble
description = 'Runs integration tests.'
group = 'verification'
useJUnitPlatform{
includeTags 'IT'
excludeTags 'unit'
}
testLogging {
events "passed", "skipped", "failed"
}
}

dockerCompose.isRequiredBy(integrationTest)

You find a full example here. Let us check what each line does:

StatementFunction
plugins --> id 'com.avast.gradle.docker-compose' version "0.14.3"Imports the gradle plugin to use docker-compose
task integrationTest(type: Test)Defines a new task of type Test (and we want to run tests)
dependsOn assembleBefore we can build the Dockerfile (using compose), we must assemble (build without test) our application
description = 'Runs integration tests.'Self-explaining
group = 'verification'Self-explaining
useJUnitPlatformAlready seen in unit tests only this time we switch the include and exclude
testLoggingLog level, known from unit tests
dockerCompose.isRequiredBy(integrationTest)Instructing gradle, that dockerCompose is required for integrationTest
Task

Add the fully-automated integration tests as described above.

Verification

Task

Run the tests locally, make sure they run. It will look something like shown below.

Unit Test

โฏ ./gradlew test
Starting a Gradle Daemon, 1 stopped Daemon could not be reused, use --status for details

> Task :test

EmployeeTest > Is too young PASSED

EmployeeTest > Old enough PASSED

EmployeeTest > Of exact age PASSED

StarterApplicationTests > contextLoads() PASSED

BUILD SUCCESSFUL in 10s
4 actionable tasks: 1 executed, 3 up-to-date

Integration-Test

โฏ ./gradlew integrationTest

> Task :compileJava
Note: /Users/i511895/SAPDevelop/abb/spring-starter/src/main/java/ch/abbts/nds/swe/swdt/starter/CustomWebSecurityConfigurerAdapter.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

> Task :composeUp
Building spring-starter
#1 [internal] load build definition from Dockerfile
#1 sha256:e1337714ae876a631a3bf45c92f841420367f90eb30d6ef5373ed94e28a5a64e
#1 transferring dockerfile: 243B 0.0s done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 sha256:32fbaf5926bedad3eaba22b0a79f92ce2d11f6f2df2a01d5052dbe2441255ebf
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/azul/zulu-openjdk-alpine:16
#3 sha256:ec9410ded58039807916e8fe0a299ec3a2e7197e9b0ce752e04977d6f91dfe97
#3 ...

#4 [auth] azul/zulu-openjdk-alpine:pull token for registry-1.docker.io
#4 sha256:c704e25d5a25d2c7d3b59d36e5eeda887364ca8b5c1c6e4866fc4573ec2e7ef4
#4 DONE 0.0s

#3 [internal] load metadata for docker.io/azul/zulu-openjdk-alpine:16
#3 sha256:ec9410ded58039807916e8fe0a299ec3a2e7197e9b0ce752e04977d6f91dfe97
#3 DONE 1.5s

#5 [1/3] FROM docker.io/azul/zulu-openjdk-alpine:16@sha256:c7ccaaba7dfac3f2f921122a9afc857938a451f740d3c9afb53499016ce344ab
#5 sha256:4c6ecb1d4dc6e3734acab6b473c1584d377df5560c68a0fd37bd8fcb3637e821
#5 DONE 0.0s

#7 [internal] load build context
#7 sha256:66222cb7c1cd63d64d2d6c03cc5be835c62af9503e3779f1348b97b70b3cc291
#7 transferring context: 22.40MB 0.6s done
#7 DONE 0.6s

#6 [2/3] RUN addgroup -S spring && adduser -S spring -G spring
#6 sha256:cbc1aef0ebe6c25c05bb97ef259dce0878a1b5b7adc7d98c911892cc573b2b4f
#6 CACHED

#8 [3/3] COPY build/libs/*T.jar app.jar
#8 sha256:41507ef1965217d13258fab725d0c5ea680134611cff7125f3d7562e1227f9cc
#8 DONE 0.1s

#9 exporting to image
#9 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#9 exporting layers 0.1s done
#9 writing image sha256:a185e2bafb07a0d69fcb64b4b07cd5d5d7b775358b7e61f4929e1ff9109e7857 done
#9 naming to docker.io/library/ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter done
#9 DONE 0.1s
Creating network "ab4f8ff4365c3530a6e3290494e6ed02_starter__default" with the default driver
Creating ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ...
Creating ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ... done
Docker Compose is now in the Docker CLI, try `docker compose up`

Will use localhost as host of spring-starter
More forwarded TCP ports for service 'spring-starter:8080 [[HostIp:0.0.0.0, HostPort:8080], [HostIp:::, HostPort:8080]]'. Will use the first one.
Probing TCP socket on localhost:8080 of 'spring-starter_1'
Waiting for TCP socket on localhost:8080 of 'spring-starter_1' (TCP connection on localhost:8080 of 'spring-starter_1' was disconnected right after connected)
Will use localhost as host of spring-starter
More forwarded TCP ports for service 'spring-starter:8080 [[HostIp:0.0.0.0, HostPort:8080], [HostIp:::, HostPort:8080]]'. Will use the first one.
Waiting for TCP socket on localhost:8080 of 'spring-starter_1' (TCP connection on localhost:8080 of 'spring-starter_1' was disconnected right after connected)
Will use localhost as host of spring-starter
More forwarded TCP ports for service 'spring-starter:8080 [[HostIp:0.0.0.0, HostPort:8080], [HostIp:::, HostPort:8080]]'. Will use the first one.
Waiting for TCP socket on localhost:8080 of 'spring-starter_1' (TCP connection on localhost:8080 of 'spring-starter_1' was disconnected right after connected)
Will use localhost as host of spring-starter
More forwarded TCP ports for service 'spring-starter:8080 [[HostIp:0.0.0.0, HostPort:8080], [HostIp:::, HostPort:8080]]'. Will use the first one.
Waiting for TCP socket on localhost:8080 of 'spring-starter_1' (TCP connection on localhost:8080 of 'spring-starter_1' was disconnected right after connected)
Will use localhost as host of spring-starter
More forwarded TCP ports for service 'spring-starter:8080 [[HostIp:0.0.0.0, HostPort:8080], [HostIp:::, HostPort:8080]]'. Will use the first one.
TCP socket on localhost:8080 of 'spring-starter_1' is ready
+------------------+----------------+----------------+
| Name | Container Port | Mapping |
+------------------+----------------+----------------+
| spring-starter_1 | 8080 | localhost:8080 |
+------------------+----------------+----------------+

> Task :integrationTest

EmployeeControllerTestIT > /employees/ returns 200 and a 3 employees PASSED

> Task :composeDown
Stopping ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ...
Stopping ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ... done
Removing ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ...
Removing ab4f8ff4365c3530a6e3290494e6ed02_starter__spring-starter_1 ... done
Removing network ab4f8ff4365c3530a6e3290494e6ed02_starter__default

BUILD SUCCESSFUL in 33s
9 actionable tasks: 9 executed

Observe what happens above:

  1. Docker Compose ups (starts) your backend
  2. JUnit runs your integration tests against the new container
  3. Docker Compose turns your container off
F* amazing

You literally mastered the most difficult part of our lecture ๐ŸŽ‰๐Ÿป๐Ÿฅ‚ This technology is so fascinating!

Automate integration testing away

You know what is missing don't you... Automate the test running away!

Task

Change your GitHub Action Workflow. Make it run both tests in two different jobs (it can do that know thanks to your work before!).

Scanning our container

Task

You learned in Lesson 6 how container scanning works. You have attended multiple lessons that run GitHub Actions. Setup another workflow, that runs every sunday night and upon every Pull Request and scans your docker image for vulnerabilities.

Scan your container

First, scan your container, you will find a ton of vulnerabilities. Switch the base image to azul/zulu-openjdk-alpine:16 to get rid of most of them.

Add the workflow

name: Container Scan
on:
schedule:
- cron: '0 2 * * 0'
pull_request:
branches:
- main
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- run: ./gradlew build
name: Build application
- run: docker build . -t ndshfswe.azurecr.io/spring-starter:${{ github.sha }}
name: Build docker image
- uses: Azure/container-scan@v0
name: Scan docker image
with:
severity-threshold: MEDIUM
image-name: ndshfswe.azurecr.io/spring-starter:${{ github.sha }}

Add this point in time, I do expect that you can teach yourself what this file does. Only one point is to note.

We use Azure/container-scan instead of Snyk because it does not require any token or API and runs locally on our ubuntu runner.

Sample

Here we go.

Note that at the point of writing (28th May 2021) one CVE (CVE-2021-31535) even in the latest Java image. We must await a fix - check the logs here to see the scans failing.