Create a TypeScript Apollo Server and Live Database with Unit Tests

A Quick Tutorial Using as an Example Neo4j and neo-forgery

Yisroel Yakovson
Neo4j Developer Blog
14 min readAug 20, 2021

--

Many of us have fallen in love with Apollo Server. It is a simplifying yet flexible abstraction layer. So many frameworks and middleware options usually combine without conflicts.

A good example is using the neo4j-driver tool. You can generate a server that magically works with your database. And you can extend the magic with neo4j/graphql. That package automatically generates queries and mutations from your typeDefs, and provides powerful directives to generate resolvers that query the database.

But when it comes to unit testing, the abstraction layer becomes a lot more imposing. How can you stub out the database calls when they are so effectively hidden in the server code?

This tutorial will enable you to create an Apollo Server using unit tests and even TDD!

If you do all of the steps, it will probably be about 30 minutes. Check out the full solution if you’d rather just look at the code or copy it file by file.

This is a follow-up to a tutorial on mocking calls to a Neo4j database using neo-forgery. The technique shown there to mock a call works beautifully when you explicitly run a query using a declared session.

But if you are using directives with an Apollo Server, you will need to mock the driver itself.

This article guides you through creating a TypeScript Apollo Server with a live database. We use Neo4j as a database for a complete live stack. Then we show you how to run integration tests with an Apollo Server. Finally, we create unit tests from the integrations tests.

This tutorial expects you to know the minimal basics of using Node.js, Apollo Server, and Neo4j. But I try to explain everything.

Steps

  1. Create an empty TypeScript project with AVA [3 minutes]
  2. Create an Apollo Server instance in TS with a live Neo4j database [2 minutes if you have a database. Maybe 10 minutes if you don’t]
  3. Create integration tests using executeOperation [5 minutes]
  4. Use neo-forgery to mock the database. [20 minutes]

1. Create a New Project

The following four steps create a TypeScript project that is ready for testing using AVA.

(1) Run these commands in a terminal to create a project with the AVA test runner using TypeScript:

(2) Add this AVA specification to your package.json to use AVA with typescript, and to specify a test directory with files to test:

Or you can just copy over the solution package.json.

(3) Add a tsconfig.json file with the following contents to enable certain things when we begin coding:

Notably, the rootDirs gives two root directories so that you can use linting also with your test files.

2. Create an Apollo Server with a Live Database

If you check out the Apollo Server Getting Started Tutorial, you’ll see simple steps for creating a books server. We’ll do almost the same thing, but we’ll use TypeScript. And we’ll recreate the data and resolvers (an enhanced version actually) with neo4j.

(1) Add some packages to work with Apollo and neo4j.

(2) Create the following src/index.ts file:

Let me explain the contents. It is actually almost the same as the one shown in the popular Getting Started With Apollo Server Tutorial. But there are three critical differences:

  • Because we will use TypeScript, we will import the type ApolloServer and use it in our declaration of server.
  • The Getting Started tutorial adds the following books array and resolver rather than a database.

We’re going to replace those with a live database. To do that, we have added: (a) a dotenv call to allow us to access database secrets; (b) required packages and boilerplate to create schema; (c) different parameters for the ApolloServer constructor, since we’ll be using schema rather than resolvers and typedefs.

  • We remove the Query declaration from our typedefs. That will be generated automatically by the neo4j/graphql package, along with some resolvers for CRUD functions with the Book type.

(3) Add a .env file at the root level which contains the credentials for some Neo4j database. If you don’t have one, you can create one for free in a few minutes with neo4j sandbox for the sake of this tutorial. If you create a sandbox, they will give you the credentials.

(4) Launch your server locally in a terminal to create some data.

Then click that link for http://localhost:4000. You should see something like this:

Click Query your server and you should enter the ApolloGraphQL Sandbox. It should look something like this:

We can now use this interface to run a first mutation that will populate our database.

Click Root in the Documentation section and then you should see mutation as an option.

Click mutation , and you should see the Fields open up to show a few mutations generated by @neo4j/graphql:

We need to create the two books in the ApolloServer tutorial in our live database. You can read about it in the documentation by clicking on createBooks if you like. In the Operations panel, enter in

Then put the proper json into the Variables panel at the bottom:

Click the Run button and you should see the results of the query:

You can now query for books as in the Getting Started tutorial, only now it’s with a live database.

3. Create Integration Tests

Before we create unit tests for our server, let’s make integration tests. The essential difference is that the integration tests will make async calls to the live database, whereas the unit tests will mock those out.

But even if you work with unit tests, you should have some integration tests anyway to make sure all is well with your database. The integration tests you can run periodically, and the unit tests you should run with every change to your code.

Fortunately, an Apollo Server comes with a built-in function executeOperation that makes integration tests a breeze.

(1) prepare the necessary files for integration testing.

  • First, create some folders for testing
  • Then we need to add an AVA config file for integration tests int-tests.config.cjs:
  • Finally, update scripts in the package.json file to include int-test:

When we want to run an integration test, we’ll be able to do so by calling npm run int-test -- <fileName>. Or we can run all of them with simply npm run int-test. Note that integration tests should be in the directory test/int, and should contain the suffix int.ts.

(2) I’m going to recommend refactoring src/index.ts to make it easier to create tests. Instead of creating server in the src/index.ts file, we’ll create a function createServer() that we import from a separate file. That way, we’ll be able to create test servers as needed, and ultimately mock servers.

If you are in a hurry, you could just copy the solution test/src directory over which combines this refactoring with another one below. Or take a few minutes and work the changes through yourself so that you understand them.

Create src/newServer.ts, which is essentially the same code lifted from src/index.ts and a new function:

Now we can remove virtually all of src/index.ts and simply call newServer there:

(3) Next let’s create our first int test 😍.

The data from our first executed query in the Apollo Sandbox is all we need for a first integration test. A bit of copying and pasting will let you create the file test/data/createBooks.ts:

Then create the following integration file as test/int/index.int.ts :

This code simply uses the AVA test runner to confirm that your server calls the createBooks resolver correctly. The executeOperation function is used to call createBooks. There are two assertions here. The first is that the result has no errors. The second is that the output is correct. Check out AVA to learn more about assertions and test generation.

You can now run npm run int-test and you should see this:

Although we won’t do the other resolvers here, you can trivially repeat the above approach for your other two mutations and for the books query. You can copy over the test/data directory and int test file to get them all. In short:

  1. run each query with sample data
  2. create a file in test/data with the results
  3. add to test/int/index.int.ts a test that makes sure the data returned is correct and there are no errors. IMPORTANT NOTE: in AVA you must use test.serial() in place of test() for any tests which must be performed in a particular order.

Check out the completed integration test file.

4 Use neo-forgery to mock the database

You could run your int test whenever you’d like, but it’s not appropriate for continuous integration (CI) or test-driven design (TDD). You’ll need real unit tests for those.

Fortunately, it’s not hard with neo-forgery.

(1) in your terminal, create a unit test directory and copy over the int test:

NOTE: it would honestly be more consistent to use the extension unit.ts rather thantest.ts. But test.ts is by far the most common convention. I do not want to use the test.ts extension with integration tests, because you could end up accidentally modifying a unit or int test when you intended the other.

Try running npm test just to confirm that you get the same success as you got with the int test. But remember that it’s still calling the database.

(2) refactor context out of newServer

For our unit tests, we’ll need a mock version of the server. To minimize duplicated code, we’ll refactorcontext to be a parameter for newServer, so that we can easily replace the live database with a mock one. That means that our src/index.ts file will now look like this:

Here’s the src/context.ts file that it will use:

Those contents were simply pulled out of src/newServer, which becomes simpler:

Just as a sanity check, you might run npm run int-test to confirm that you get:

That is easily fixed by updating test/int/index.int.ts to use the context:

Run it again, and you’ll see a reassuring green success message!

(3) Now to create our mock context! 😃

We will need a “Query Set”, which is an array of the QuerySpec type. Check out the neo-forgery documentation for more. Let’s create a placeholder in a separate file test/unit/querySet.ts:

Then we can create a file test/unit/mockContext.ts:

This file exports a context that contains a mock driver instead of the real thing. This mock driver will check only for the contents of querySet when a query is made, and if no matching query is found an error is returned. Since there are currently none, we can expect an error the first time we use it. But that’s okay because when you start using TDD you learn to thrive on failure! 😉

(4) Run the test with the mock server

Now, this is cool… we can make a unit test by copying our int test with a tiny change:

Now just change the import of context to be our mock context:

[If you are using the complete set of int tests, you should ideally replace any test.serial with a simple test, because for the unit tests, there will be no need to be executed in a particular sequence given that they have no side effects.]

Then we can run the unit test:

Yeah, we failed… but it’s a good kind of failure because it tells us what to fix! 👍 You see that @neo4j/graphql generated a query that we have to add to our set.

As the neo-forgery documentation explains, a QuerySpec requires a query, any params, and a result. We can copy into our file test/unit/querySet.ts the query string and params.

The result will require using the neo4j data browser. First, add the params by using the params: command with the outermost curly braces removed:

The browser should confirm that they were set:

Then run the query, and click on the CODE button on the left. Open Responses, and you can copy the resulting array:

You can use the utility wrapCopiedResults from neo-forgery to

The new test/unit/querySet.ts looks like this:

Running the test again gives us a reassuring green message:

It is trivial to follow those steps for the other errors as well.

Final Words

If you’ve done these steps, you know how to create an Apollo Server with a live Neo4j database and to create both integration and unit tests. That means that you can build an Apollo Server using TDD!

You probably would benefit from the tutorial on mocking calls to a neo4j database if you want to see more detail about using neo-forgery.

A good follow-up tutorial would be incorporating in auth if there’s enough interest in this one. So please clap if you would like to see more!

--

--