Docker on Amazon Web Services
上QQ阅读APP看书,第一时间看更新

Creating acceptance tests

Now that the application is configured correctly, the final task to configure for the release stage is to define acceptance tests that verify the application is working as expected. Acceptance tests are all about ensuring the release image you have built works in an environment that is as close to production as possible, within the constraints of a local Docker environment. At a minimum, if your application is a web application or API service, such as the todobackend application, you might just verify the application returns a valid HTTP response, or you might run through key features, such as creating an item, updating an item, and deleting an item.  

For the todobackend application, we will create a few basic tests to demonstrate the approach, using a tool called BATS (Bash automated test system). BATS is great for system administrators who are more comfortable using bash, and leverages out-of-the-box tools to execute tests.

To get started with BATS, we need to create a test script, called acceptance.bats, in the src folder of the todobackend repository using the BATS syntax, which you can read more about at https://github.com/sstephenson/bats:

setup() {
url=${APP_URL:-localhost:8000}
item='{"title": "Wash the car", "order": 1}'
location='Location: ([^[:space:]]*)'
curl -X DELETE $url/todos
}

@test "todobackend root" {
run curl -oI -s -w "%{http_code}" $APP_URL
[ $status = 0 ]
[ $output = 200 ]
}

@test "todo items returns empty list" {
run jq '. | length' <(curl -s $url/todos)
[ $output = 0 ]
}

@test "create todo item" {
run curl -i -X POST -H "Content-Type: application/json" $url/todos -d "$item"
[ $status = 0 ]
[[ $output =~ "201 Created" ]] || false
[[ $output =~ $location ]] || false
[ $(curl ${BASH_REMATCH[1]} | jq '.title') = $(echo "$item" | jq '.title') ]
}

@test "delete todo item" {
run curl -i -X POST -H "Content-Type: application/json" $url/todos -d "$item"
[ $status = 0 ]
[[ $output =~ $location ]] || false
run curl -i -X DELETE ${BASH_REMATCH[1]}
[ $status = 0 ]
[[ $output =~ "204 No Content" ]] || false
run jq '. | length' <(curl -s $APP_URL/todos)
[ $output = 0 ]
}

The BATS file includes a setup() function and a number of test cases, which are each prefixed with the @test marker. The setup() function is a special function that will be run before each test case, and is useful for defining common variables and ensuring the application state is consistent before each test. You can see that we set a few variables that are used in the various test cases:

  • url: Defines the URL of the application under test. This is defined by the APP_URL environment variable, defaulting to localhost:8000 if APP_URL is not defined.
  • item: Defines a test Todo item in JSON format that is created via the Todos API during the tests.
  • location: Defines a regular expression intended to locate and capture the value of the Location header that is returned in the HTTP response whenever you create a Todo item.  The ([^[:space:]]*) portion of the regular expression captures zero or more characters until whitespace (as designated by the [:space:] indicator) is encountered. For example, if the location header was Location: http://localhost:8000/todos/53, the regular expression will capture http://localhost:8000/todos/53.
  • The curl command: The final setup task is to delete all todo items in the database, which you can do by sending a DELETE request to the /todos URL. This ensures the todobackend database is clean on each test run, reducing the likelihood of different tests introducing side effects that break other tests.

The BATS file next defines several test cases:

  • todobackend root: This includes the run function, which runs the specified command and captures the exit code of the command in a variable called status, and the output of the command in a variable called output.  For this scenario, the test runs a special configuration of the curl command that captures only the HTTP status code that is returned, and then verifies the curl command completed successfully by calling [ $status = 0 ]and that the returned HTTP status code was a 200 code by calling [ $output = 200 ]. These tests are regular shell test expressions, and are the equivalent of the canonical assert statement found in many programming languages.
  • todo items returns empty list: This test case uses the jq command to pass the output calling the /todos path. Note that because you can't use pipes in conjunction with the special run function, I have used the bash process substitution syntax, <(...), to make the output of the curl command appear as a file that is being read by the jq command.
  • create todo item: This first creates a todo item, checks whether the returned exit code is zero, and then uses a bash conditional expression (as indicated by the [[...]] syntax) to verify that the output of the curl command includes 201 Created in the HTTP response, which is a standard response when creating an item. When using the bash conditional expressions, it is important to note that BATS will not detect an error if the conditional expression fails, hence we use the || false special syntax, which is only evaluated in the event the conditional expression fails and returns a non-zero response of false, causing the test case to fail if the test expression fails. The conditional expressions use the =~ regular expression operator (this operator is not available in conditional expressions, hence our use of bash test expressions), with the second conditional expression evaluating the location regular expression defined in the setup function. The final command uses the special BASH_REMATCH variable that includes the results of the most recent conditional expression evaluation, which in this case is the URL matched in the Location header. This allows us to capture the returned location when we create a Todo item, and verify that the created item matches the item that we posted.
  • delete todo item: This creates a Todo item, captures the location returned for the item, deletes the item, and then verifies that the item was in fact deleted by verifying the number of Todo items in the database is zero after the deletion. Recall that the setup function runs before each test case, which clears all Todo items, hence at the beginning of this test case the Todo item count will always be zero, and the action of creating and then deleting an item should always return the count to zero.  The various commands used in this test case are based upon the concepts introduced in the create todo item test case, hence I won't describe each command in detail.

Now that we have define a suite of acceptance tests, it's time to modify the Docker environment to support the execution of these tests once the application is started successfully.

We first need to add the curl, batsand jq packages to the Dockerfile at the root of the todobackend repository:

# Test stage
FROM alpine AS test
LABEL application=todobackend
...
...
# Release stage
FROM alpine
LABEL application=todobackend

# Install dependencies
RUN apk add --no-cache python3 mariadb-client bash curl bats jq
...
...

Next we need to add a new service called acceptance to the docker-compose.yml file, which will wait until the app service is healthy and then run acceptance tests:

version: '2.4'

volumes:
public:
driver: local

services:
test:
...
...
release:
...
...
app:
extends:
service: release
depends_on:
db:
condition: service_healthy
volumes:
- public:/public
healthcheck:
test: curl -fs localhost:8000
interval: 3s

retries: 10
ports:
- 8000:8000
command:
- uwsgi
- --http=0.0.0.0:8000
- --module=todobackend.wsgi
- --master
- --check-static=/public
acceptance:
extends:
service: release
depends_on:
app:
condition: service_healthy
environment:
APP_URL: http://app:8000
command:
- bats
- acceptance.bats
migrate:
...
...
db:
...
...

We first add a healthcheck property to the app service, which uses the curl utility to check connectivity to the local web server endpoint.  We then define the acceptance service, which we extend from the release image and configure with the APP_URL environment variable, which configures the correct URL the acceptance tests should be executed against, whilst  the command and depends_on properties are used to run the acceptance tests once the app service is healthy.

With this configuration in place, you now need to tear down the current environment, rebuild all images, and perform the various steps to get the application up and running, except when you get to the point where you are about to run the docker-compose up app command, you should now run the docker-compose up acceptance command, as this will automatically start the app service in the background:

> docker-compose down -v
...
...
> docker-compose build
...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
...
...
> docker-compose up acceptance
todobackend_db_1 is up-to-date
Creating todobackend_app_1 ... done
Creating todobackend_acceptance_1 ... done
Attaching to todobackend_acceptance_1
acceptance_1 | Processing secrets []...
acceptance_1 | 1..4
acceptance_1 | ok 1 todobackend root
acceptance_1 | ok 2 todo items returns empty list
acceptance_1 | ok 3 create todo item
acceptance_1 | ok 4 delete todo item
todobackend_acceptance_1 exited with code 0

As you can see, all tests pass successfully, as indicated by the ok status for each test.