Sunday, June 7, 2020

Unit Tests - CI/CD Series

In the previous post we created a simple REST service built on top of akka-http. However, as practiced with professional software development, we want to add unit tests to our service to ensure everything is working as planned. These unit tests provide several things for our service:
  1. Automated testing
  2. Reproducible tests
  3. Software assurance
Unit Tests

To build the unit tests for our service, we will leverage:
  • ScalaTest
    • The base testing framework (similar to JUnit for Java)
  • akka-http
    • akka-http provides test harnesses that integrate directly with ScalaTest
With these testing libraries, we can perform tests directly against the endpoints of our service instead of using objects. Also, both the requests and responses are full payloads so we can assert things such as:
  • Response code
  • Application type
  • Response body
This will help provide a full end-to-end unit test instead of directly calling an object and ignoring the serialization aspect of the test.

The simplest endpoint in our service is the health check - it only returns 200. A unit test for this endpoint is as simple as:

it should "return OK for /health" in {
Get("/health") ~> healthCheck.route ~> check {
status shouldBe StatusCodes.OK
}
}

In this test, the actions performed are:
  1. Send a GET request with the path "/health"
  2. The request goes to the route defined in the object "healthCheck"
  3. Assert that the returned status is OK (200)
We can use this same kind of test setup for a more complex case (such as adding a guest to our guestbook):

it should "add a guest" in {
val guestBook = new GuestBook

Post("/guests").withEntity(guestEntity) ~> guestBook.route ~> check {
status shouldBe StatusCodes.Created
}

Get("/guests") ~> guestBook.route ~> check {
status shouldBe StatusCodes.OK
contentType shouldBe ContentTypes.`application/json`
entityAs[Guests] shouldBe Guests(List(guest))
}
}

This test has a similar setup as the one above, just with a few more steps:
  1. Send a POST request with the guest data (defined outside of snippet)
  2. The request goes to the route defined in the object "guestBook"
  3. Assert that the returned status is Created (201)
  4. Send a GET request with the path "/guests"
  5. The request goes to the route defined in the object "guestBook"
  6. Assert that the returned status is OK (200)
  7. Assert that the returned content type is "application/json"
  8. Assert that the returned entity is our guest we added previously (wrapped in a list)
Continuous Integration

Now that we have unit tests available, we can hook up our continuous integration using:
For this simple REST service, the actions we want to perform are:
  • Run unit tests on all pull requests to the "master" branch
  • Run unit tests on all pushes to the "master" branch
  • Running unit tests consists of
    • Using a docker image which has sbt installed
    • Running "sbt test"
The entirety of the above can be expressed with just a few lines of a YAML definition:

name: SBT CI

# Run SBT tests on pushes and pull requests to master branch
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
runs-on: ubuntu-latest
# The specific container to use
# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainer
# https://hub.docker.com/r/hseeberger/scala-sbt/
container: hseeberger/scala-sbt:8u222_1.3.5_2.13.1

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2

# Runs a set of commands using the runners shell
- name: Run Unit Tests
run: sbt test

Conclusion

Given the above unit tests and GitHub Actions definition, we now have a system which will run all unit tests on every build. This will not only ensure our system performs as expected, but also alert us when a build breaks for any reason.

All of the changes mentioned above (and more) can be found on this pull request.

No comments:

Post a Comment