The test harness

Using the scaffolding tool in Part 1 created a test/ folder at the project root, with the file structure below:

test/

|--harness

|--app.dart

example_test.dart

The test harness harness/app.dart is responsible for starting and stopping our application. We see this in effect when looking at this snippet in test/example_test.dart :

The application is started before running all our tests and stopped immediately afterwards. Our test harness replicates bin/main.dart with these exceptions:

A port 0 is specified so that our tests can run on any available port A separate configuration file(config.src.yaml) is given containing test-specific data The runOnMainIsolate option is set to true when application.start is called, running our test on a main thread. This disables multi-threading so that we can access the application’s state and services to perform our assertions. A TestClient is instantiated to provide a HTTP client for performing requests to our APIs. Using this will give us test responses for making our assertions on. A method for stopping the application is provided to be invoked after all tests are run

In order to run our tests, let’s refactor our solution in Part 3, starting with our database configuration. This is to allow flexibility to support testing and production environments.

1. Configure the database

Looking again at FaveReadsSink in lib/fave_reads_sink.dart , the same Postgres details will be used in our test and production environments (*Yikes!*)

The use of configuration files help mitigate this by separating test and production information. In our project we will be working with config.src.yaml and config.yaml files at the root level.

Let’s start by amending these files with our db connection information:

# config.src.yaml - for test environment

database:

username: dartuser

password: dbpass123

host: localhost

port: 5432

databaseName: fave_reads_test

isTemporary: true

And in our config.yaml file:

# for production environment

database:

username: dartuser

password: dbpass123

host: localhost

port: 5432

databaseName: fave_reads

isTemporary: false

This allows us to make the following modifications to FaveReadsSink :

Our configuration file path is specified by the configurationFilePath property on the Application constructor in bin/main.dart and test/harness/app.dart . Our FaveReadsSink class is instantiated inside an application object, via which it receives the configuration path in the appConfig argument of our request sink constructor.

We then extract our configuration information by extending FaveReadsConfiguration , a subclass of the ConfigurationItem helper class. This parses the configuration file as a Map.

After the request sink, let’s define our configuration item:

The database property maps to the same key in our configuration file. This is what exposes our database information to be accessed like such: config.database.username

2. Extract SchemaBuilder into utility file

Let’s move the createDatabaseSchema method into a separate file to also be used by our test harness:

We now have the second parameter isTemporary to be set by our configuration files. This option determines whether our data is persisted or not. We set this to true for our tests.

Let’s now import this utility back into lib/fave_reads_sink.dart :

3. Set up the testing database

Open the psql tool and run the SQL query below:

CREATE DATABASE fave_reads_test;

CREATE USER dartuser;

ALTER USER dartuser WITH password 'dbpass123';

GRANT ALL ON database fave_reads_test TO dartuser;

You can skip lines 2 and 3 if you already did this in Part 3.

4. Write some tests

Rename example_test.dart to books_controller_test.dart and replace its contents with the below:

On line 39 we call the discardPersistentData method in order to disconnect and reconnect the database, using the same setup data for each test we run. Since our test data is temporary, it only lasts for the duration of the connection.

We still need to create this method in our test harness:

// test/harness/app.dart class TestApplication {

...

Future discardPersistentData() async {

await ManagedContext.defaultContext

.persistentStore.close();

await createDatabaseSchema(

ManagedContext.defaultContext, true);

}

...

}

Our tests are contained within a main() top-level function as required by Dart in order to run our tests. The setUp function creates a list of Book types and using the Query<T> object we populate the database for each test. Calling the query object reopens the database during the test.

Let’s create a test by replacing the //...tests to go here comment with the snippet below:

The group function is used for categorising related tests, similar to having the describe block if you’ve worked with the Jasmine BDD framework and test is similar to the it block.

Some further things to take note of:

Our first test creates a request from our TestClient object, performs the GET operation and runs our assertion on the response using the expectResponse matcher method. It accepts the response, status code and assertion under the body named parameter. everyElement is another matcher method that allows us to run a check against each item in the response body, assuming it’s a list. partial makes an assertion against specific keys, provided the list item is a Map. We use this to save us checking every single key. isString and isInteger are other inbuilt getters for ensuring the type is what we expect

Here’s the next one for a POST request:

The post request object accepts a body through the json property. This assumes the payload is a JSON string.

Below is our full automated test:

We can run our tests by doing:

dart test/books_controller_test.dart

All tests passed! 🎉

Conclusion

Our APIs are now covered by tests. I hope you learnt something useful and may find it worth considering Aqueduct for your next project.

Check out the reading materials below to understand Aqueduct testing in further detail. As always, feedback is welcome. Let me know what you liked, disliked and what you’d want to see next. I’d really be grateful for that.

And this concludes Part 4 of the series. The source code is available on github. Like and follow me if you enjoyed this article for more content on Dart. Check out the bonus content now available.

Further reading