Deno by example: Content server — Part 4 Integration & Performance testing

Mayank C
Tech Tonic

--

Welcome to the medium course:

Learn Deno by example — Content server.

This is a medium course that takes the user from writing, testing, formatting, logging, documenting, and deploying an application in Deno. This rapid course consists of six sections.

Here are the course contents:

In section 1, we’ve gone through the content server application along with some manual tests. In section 2, we’ve used Deno’s built-in formatter and linter to improvise on the content server application. In section 3, we’ve identified, and written unit tests to test the smallest possible units in the application. In this section, we’ll go a bit high-level and learn about writing integration & performance tests to validate the application as a black box. We’re progressively enhancing the application. It’s advisable to read sections 1, 2, & 3 before proceeding with this section. Let’s get started!

Integration testing

Unlike unit testing that focuses only on the units of the application, integration testing is done to test the modules/components when integrated to verify that they work as expected, i.e. to test the modules which are working fine individually does not have issues when integrated. In other words, integration testing is a level of software testing where individual units / components are combined and tested as a group. The purpose of this level of testing is to expose faults in the interaction between integrated units.

To perform integration testing, the application would likely be deployed at a target system along with other services. For example-if an application is dependent on another service as well as a database, the integration testing would be performed after deploying the application in an instance/container that would have access to the required services.

Basics

We’ll use Deno’s same test runner to write integration tests. Once the application has been deployed on the target, there would be an endpoint available to make requests. The integration test suite would make requests to the endpoint and process the results. There are two differences between integration testing and unit testing:

  • No imports: There won’t be any direct imports as, possibly, remotely deployed service would be tested
  • Single interface: Unlike unit testing that had multiple interfaces like Request/Response, string/content, and headers/boolean, integration testing has a single Request/Response interface

A typical integration test

All the integration tests would follow the same structure. A typical integration test would have two parts:

  • Fetch: Make a fetch call with test specific data
  • Response: Process & check response

Here is the skeleton of a typical integration test for the content server application:

Deno.test('some integration test', async () => {
const res=await fetch('someUrl');
const resData=await res.arrayBuffer(); //or, res.text()/res.json()/etc.
//check status, headers, and response body
});

The above would be the common structure for all the integration tests for the content server application. For different application, there might be a need to prepare a request body, but not for content server. This makes integration testing a bit simple.

Integration tests

There is no limit or rule of thumb on how many integration tests should be written. The purpose is to test the application as it integrate with other services. We’ll write some negative and some positive tests that covers most of the application code.

Note that it may not be possible to cover all the code in integration tests

The test setup is the following:

  • Content server application is deployed at http://localhost:8080
  • The API keys and data files of unit test would be reused
$ deno run --allow-net=:8080 --allow-read=./ main.ts
Content server started...

Here is the list of all integration tests:

As we can see that the integration tests are very similar to controller tests except for the fetch API call.

The integration tests are located in a separate directory called integration. This way they don’t run with unit tests.

$ ls -RF
authService.ts controller.ts main.ts test/
consts.ts fileService.ts router.ts cfg.json
./test:
integration/ unit/
./test/integration:
integration_test.ts
./test/unit:
authService_test.ts data/ router_test.ts
controller_test.ts fileService_test.ts
./test/unit/data:
apiKeys.json pdfFile.pdf textFile.txt

That’s all. Let’s run the integration tests using deno test.

$ deno test --allow-net=:8080 test/integration/
running 7 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/integration/integration_test.ts
test No auth header ... ok (16ms)
test Empty auth header ... ok (12ms)
test Invalid token ... ok (9ms)
test POST ... ok (8ms)
test Get directory ... ok (10ms)
test Text file ... ok (15ms)
test PDF file ... ok (14ms)
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (110ms)

All the 7 tests have passed. All good so far!

That’s all about integration testing. We’ve intentionally kept the number of integration tests low, but in real applications, it might run in hundreds. Let’s move on to the second and last part of this section, performance testing.

Performance testing

Performance testing is an integral part of software development. If a software can’t perform at scale, it’s not of much use. Generally, performance testing is carried out by specialized tools like Jmeter, autocanon, wrk, etc. Some of these tools are sophisticated and often needs additional knowledge & setup. Some have UI, some have scripts, some have config files, etc.

While doing integration testing (from the previous section), it’d be good to be able to do a round of basic performance testing using the same tool that we’ve used for unit & integration testing. While Deno doesn’t directly support performance testing, the async nature of the runtime can be used to run a very basic load test. Note that this is still in early development phase, and developers are looking to run a quick & basic test. The usual performance and longevity tests using sophisticated tools would still run, but at a later point of time.

Basics

The basic idea behind running a simple performance test is to initialize & execute multiple promises in parallel. There are two steps:

  • Create ‘N’ promises with inline function that would make a fetch request (new Promise)
  • Execute ‘N’ promises at the same time (this step makes it parallel) (Promise.all)
  • Repeat as many times as needed

Here is the skeleton code of a performance test function:

Deno.test('concurrency=10, repeat=1', async ()=>{
const p=new Array(10).fill(new Promise(async (resolve)=>{
const res=await fetch('someURL');
const resData=await res.text(); //or, res.arrayBuffer()
resolve(res.status);
});
const responses=await Promise.all(p);
//check status of all responses
});

Note that there is still a single thread, but the async nature makes it run in parallel because the deployed application can’t match up with the speed of local promise execution. Without using any other software, scripts, tools, a basic performance test can be executed in the early phase of development.

Performance tests

As this is still a basic & early stage performance test, the performance tests cases can be kept simple. We can have a loop, allocate ‘N’ promises, execute ‘N’ promises, and continue.

For each promise used in the performance test runs, we’ll check the following:

  • status should be 200
  • Length of data received should be 69273 bytes as we’ll use pdfFile.pdf for all the performance tests

Here are all the performance tests:

The performance tests are located in the same directory as integration tests. This way they can run together.

$ ls -RF
authService.ts controller.ts main.ts test/
consts.ts fileService.ts router.ts cfg.json
./test:
integration/ unit/
./test/integration:
integration_test.ts performance_test.ts
./test/unit:
authService_test.ts data/ router_test.ts
controller_test.ts fileService_test.ts
./test/unit/data:
apiKeys.json pdfFile.pdf textFile.txt

That’s all. Let’s run the performance tests using deno test.

$ deno test --allow-net=:8080 test/integration/performance_test.ts 
running 3 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/integration/performance_test.ts
test concurrency=100, repeat=100 ... ok (46ms)
test concurrency=1000, repeat=1000 ... ok (709ms)
test concurrency=10000, repeat=10000 ... ok (10s)
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (11s)

All the performance tests have passed. Across all tests, we’ve received the file 101,010,000 times in 11 seconds. This is good enough for initial performance tests.

We can also run all the integration and performance tests together:

$ deno test --allow-net=:8080 test/integration
running 7 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/integration/integration_test.ts
test No auth header ... ok (15ms)
test Empty auth header ... ok (10ms)
test Invalid token ... ok (8ms)
test POST ... ok (8ms)
test Get directory ... ok (9ms)
test Text file ... ok (13ms)
test PDF file ... ok (10ms)
running 3 tests from file:///Users/mayankc/Work/source/deno-course-content-server/test/integration/performance_test.ts
test concurrency=100, repeat=100 ... ok (37ms)
test concurrency=1000, repeat=1000 ... ok (640ms)
test concurrency=10000, repeat=10000 ... ok (10s)
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (10s)

All 10 tests have passed!

That’s all about running very basic performance tests early in development.

We’re done with the fourth section of the course.

Here is the map of this rapid medium course:

In the next section, we’ll over some other important aspects like logging, tracking, and documenting the content server application.

--

--