Before we build out the individual persons pages, we will want to write some tests for the pieces that have already been created. This issue will focus on the creation of backend tests for all /persons routes. While many of the existing backend test files could help you, there are some inconsistencies in older test files. As such, I would recommend looking over the page_content.test.js file for reference as it has fairly relatable pieces and follows best practices.
Split up by route, here are the tests I would like to see written:
GET /persons/:letter
[ ] Returns 200 on successful retrieval
[ ] Returns 401 if user not logged in
[ ] Returns 403 if user does not have permission
[ ] Returns 500 on failed retrieval
POST /persons/occurrences/count
[ ] Returns 200 on successful count retrieval
[ ] Returns 401 if user not logged in
[ ] Returns 403 if user does not have permission
[ ] Returns 500 on failed count retrieval
GET /persons/occurrences/texts
[ ] Returns 200 on successful texts retrieval
[ ] Returns 401 if user not logged in
[ ] Returns 403 if user does not have permission
[ ] Returns 500 on failed texts retrieval
PATCH /persons/disconnect
[ ] Returns 204 on successful disconnect
[ ] Returns 401 if user not logged in
[ ] Returns 403 if user does not have permission
[ ] Returns 500 on failed disconnect
Backend Testing Basics
The basic premise of backend testing is sending fake requests to the various backend routes and verifying that each request results in the correct behavior and returns the expected response.
The describe function creates a test suite, or a collection of related tests. The it function creates a single test. As such, you will create 4 describe test suites that correspond with each of the 4 backend routes in the persons.ts file. Within each of those 4 test suites, you will create 4 it tests as indicated above, resulting in 16 total tests in the file.
To create a test suite, use the describe function. The first parameter takes a string that is the name of the suite. Simply use the suite labels I posted above that correspond with the routes. The second parameter takes a function within which you will write the test logic.
Before you create the it function tests, there are few things you'll have to do to get each test suite set up.
First and foremost, you will create a constant called PATH that basically just stores the path of the route you are testing. Make sure to use string interpolation to include the API_PATH prefix as seen in the examples. Any path parameters, as indicated by a : in the route, must be replaced with a valid parameter value. This would include :letter in the first route you're testing.
Next, create a function called sendRequest that uses the imported request function to send a fake request when called. Look at the sendRequest function in the various test suites in the example. Make sure that you use the correct HTTP Method function (GET, POST, PATCH, etc). If the route you are testing expects the request to have a Body, make sure to use the .send() function to send a Body Object that matches the format expected. There is an example of this in the recommended reference file. Finally, any route that requires a user to be logged in to access it (for /persons all routes require this), you will add .set('Authorization', 'token') to mock the existence of an auth token.
This sendRequest function will be used in each of the tests to send the request to the route being tested. In some tests, you will instead write a different function to send the request so that you can cause different behavior (ex: not setting an auth token to test what would happen if the user wasn't logged in).
Next, it's important that you understand the serviceLocator concept when writing tests. We've talked about it enough that I'm confident that you have a basic understanding of how it works. Remember that when tests are run, the serviceLocator is empty, meaning that any function that is retrieved from the serviceLocator within the code we are testing will not exist at runtime. That is, unless we set each of those functions to be a mocked version that we create.
For each route, look through and see exactly what Objects are retrieved using the sl.get function. NOTE: This includes anything that is retrieved from the serviceLocator inside of any router-level middleware. You will create a mock Object for each of these. Then, for each of those Objects retrieved using sl.get, look to see what functions they each use in the code. For each of those functions, you will create a mock function in the corresponding Object. That may have sounded confusing, but you can see in the example that it's quite simple. Just make sure that the mocked functions have a mockResolvedValue that fits the data type that would be expected.
Next, create a setup function. Within this function you will simply use sl.set to set the serviceLocator Objects to be the mock Objects you just created. That way they aren't empty when the test runs.
Finally, add beforeEach(setup). so that the setup function is called before each test in the suite.
Now you should have all the setup out of the way! Now you can write the actual tests!
As mentioned previously, you create a test by calling the it function. The first parameter is a string that labels the tests, usually by saying what "it" should do. The second parameter is an asynchronous function that contains the test logic.
Within each test, you will:
First, do any sl.set overrides you need to. Most of the time you can skip this step because you can just rely on the values set in the setup function, but every once in a while you will want to override that value. An example of this is when you want to force one function to fail to trigger a 500 error. You simply call sl.set within the test and set new values which will override the previously set ones just for this one function.
Second, send the request. Most of the time you will simply call await sendRequest(), calling the function you already created and setting its response to a new response constant. Sometimes though, you will have to manually type out a different function to send the request to trigger different behavior, like not setting an auth token.
Finally, you will use the expect function to list a few things that you expect to be true. This is the meat of the test because it is actually what has to pass for the test to pass. Every expect function must return true in order for the test to pass. For many of these, you will simply need to check that the response.status is what you expect, but sometimes you will also want to verify that a certain function was called.
With that summary, you should be able to at least get the ball rolling with backend tests. Of course, let me know if you run into any issues or need any help!
Note: Testing with permissions can be tricky to figure out a first, but it's quite simple once you get a hang of it. As stated previously, you must mock out anything retrieved by the serviceLocator in the backend route itself AND the router-level middlewares it uses. You'll notice that the permissionsRoute middleware calls PermissionsDao.getUserPermissions which returns an array of permissions that the logged-in user has. As such, by setting mocked versions of that function, you can control what permissions are granted in the test. There are no examples of this in the page_content.test.js file I recommended you reference (there are no permission-specific routes there). As such, you may have to look at some of the other files to see examples of this.
Before we build out the individual persons pages, we will want to write some tests for the pieces that have already been created. This issue will focus on the creation of backend tests for all
/persons
routes. While many of the existing backend test files could help you, there are some inconsistencies in older test files. As such, I would recommend looking over thepage_content.test.js
file for reference as it has fairly relatable pieces and follows best practices.Split up by route, here are the tests I would like to see written:
GET /persons/:letter
POST /persons/occurrences/count
GET /persons/occurrences/texts
PATCH /persons/disconnect
Backend Testing Basics
The basic premise of backend testing is sending fake requests to the various backend routes and verifying that each request results in the correct behavior and returns the expected response.
The
describe
function creates a test suite, or a collection of related tests. Theit
function creates a single test. As such, you will create 4describe
test suites that correspond with each of the 4 backend routes in thepersons.ts
file. Within each of those 4 test suites, you will create 4it
tests as indicated above, resulting in 16 total tests in the file.To create a test suite, use the
describe
function. The first parameter takes a string that is the name of the suite. Simply use the suite labels I posted above that correspond with the routes. The second parameter takes a function within which you will write the test logic.Before you create the
it
function tests, there are few things you'll have to do to get each test suite set up.First and foremost, you will create a constant called
PATH
that basically just stores the path of the route you are testing. Make sure to use string interpolation to include theAPI_PATH
prefix as seen in the examples. Any path parameters, as indicated by a:
in the route, must be replaced with a valid parameter value. This would include:letter
in the first route you're testing.Next, create a function called
sendRequest
that uses the importedrequest
function to send a fake request when called. Look at thesendRequest
function in the various test suites in the example. Make sure that you use the correct HTTP Method function (GET
,POST
,PATCH
, etc). If the route you are testing expects the request to have a Body, make sure to use the.send()
function to send a Body Object that matches the format expected. There is an example of this in the recommended reference file. Finally, any route that requires a user to be logged in to access it (for/persons
all routes require this), you will add.set('Authorization', 'token')
to mock the existence of an auth token.This
sendRequest
function will be used in each of the tests to send the request to the route being tested. In some tests, you will instead write a different function to send the request so that you can cause different behavior (ex: not setting an auth token to test what would happen if the user wasn't logged in).Next, it's important that you understand the
serviceLocator
concept when writing tests. We've talked about it enough that I'm confident that you have a basic understanding of how it works. Remember that when tests are run, theserviceLocator
is empty, meaning that any function that is retrieved from theserviceLocator
within the code we are testing will not exist at runtime. That is, unless weset
each of those functions to be a mocked version that we create.For each route, look through and see exactly what Objects are retrieved using the
sl.get
function. NOTE: This includes anything that is retrieved from theserviceLocator
inside of any router-level middleware. You will create a mock Object for each of these. Then, for each of those Objects retrieved usingsl.get
, look to see what functions they each use in the code. For each of those functions, you will create a mock function in the corresponding Object. That may have sounded confusing, but you can see in the example that it's quite simple. Just make sure that the mocked functions have amockResolvedValue
that fits the data type that would be expected.Next, create a
setup
function. Within this function you will simply usesl.set
to set theserviceLocator
Objects to be the mock Objects you just created. That way they aren't empty when the test runs.Finally, add
beforeEach(setup)
. so that thesetup
function is called before each test in the suite.Now you should have all the setup out of the way! Now you can write the actual tests!
As mentioned previously, you create a test by calling the
it
function. The first parameter is a string that labels the tests, usually by saying what "it" should do. The second parameter is an asynchronous function that contains the test logic.Within each test, you will:
First, do any
sl.set
overrides you need to. Most of the time you can skip this step because you can just rely on the values set in thesetup
function, but every once in a while you will want to override that value. An example of this is when you want to force one function to fail to trigger a 500 error. You simply callsl.set
within the test and set new values which will override the previously set ones just for this one function.Second, send the request. Most of the time you will simply call
await sendRequest()
, calling the function you already created and setting its response to a newresponse
constant. Sometimes though, you will have to manually type out a different function to send the request to trigger different behavior, like not setting an auth token.Finally, you will use the
expect
function to list a few things that you expect to be true. This is the meat of the test because it is actually what has to pass for the test to pass. Everyexpect
function must returntrue
in order for the test to pass. For many of these, you will simply need to check that theresponse.status
is what you expect, but sometimes you will also want to verify that a certain function was called.With that summary, you should be able to at least get the ball rolling with backend tests. Of course, let me know if you run into any issues or need any help!
Note: Testing with permissions can be tricky to figure out a first, but it's quite simple once you get a hang of it. As stated previously, you must mock out anything retrieved by the
serviceLocator
in the backend route itself AND the router-level middlewares it uses. You'll notice that thepermissionsRoute
middleware callsPermissionsDao.getUserPermissions
which returns an array of permissions that the logged-in user has. As such, by setting mocked versions of that function, you can control what permissions are granted in the test. There are no examples of this in thepage_content.test.js
file I recommended you reference (there are no permission-specific routes there). As such, you may have to look at some of the other files to see examples of this.