When you start to write tests, you could spread many duplicated rows of your code all over the tests. As an example of testing endpoints of the backend, I use the SuperTest. It provides you with the ability to send HTTP requests to specific REST APIs.
For the backend, there can be some general things in the requests. For example, the headers of the request. I guess you will be required to include some common headers that are used everywhere in your API.
export const commonHeaders = {
[AUTHORIZATION_HEADER]: AUTH_HEADER,
'Content-Type': 'application/json',
};
The headers can be set to the SuperTest request object on the creation. Then it will be nice to have a common function for the tests to create a ‘request’ object. And fill the object with all needed headers.
export function createSuperTestRequest(useExternalToken = false): SuperAgentTest {
const request = agent(PlatformTest.callback());
request.set(commonHeaders);
if (useExternalToken) {
request.set(EXTERNAL_TOKEN_HEADER, EXTERNAL_TOKEN);
}
return request;
}
The createSuperTestRequest
function has a useExternalToken
parameter, but it can be something more complex like an options = { useExternalToken: false, cookies: {…}}
object. So, that approach allows you to create some complex ‘request’ in one place.
Next, it would be great to have all requests related to the specific controller in one class. It helps to reuse the same endpoint request between multiple tests. And each method can be specified with additional data like timeout(2000)
.
export class UserApiClient {
constructor(private readonly request: SuperTest<Test>) {}
async create(name: string): Promise<Response> {
const data = name === '' ? {} : { name };
return this.request.post(USER_URL).timeout(2000).send(data);
}
...
}
In some endpoints, you will use Query parameters; you can put such parameters inside the request string. However, there is a specific function query()
. It works like HashSet as well as set()
function for headers.
public get(userId: number, includeCounters = false) {
return this.request
.get(USER_URL)
.query({ includeCounters: includeCounters });
}
Inside the tests for the controller, you will create the SuperTest object and the UserApiClient class, and provide the ‘request’ object to it.
describe('Your API tests', () => {
let userApiClient: UserApiClient;
before(PlatformTest.bootstrap(Server));
before(() => {
const request = createSuperTestRequest();
apiClient = new UserApiClient(request);
});
...
}
When I write tests, I come across scenarios where I need to make stubs on objects that were initialized inside the functions. The tricky thing here is to properly make a stub on what is needed. Sinon library comes to help with the amazing feature of object prototyping in JS.
sinon.stub(RulesClient.prototype, 'send').resolves({
rules
});
In this way, you can assign whatever result you want from the async function, and no need to worry about how to inject the mock object inside some other class.
Also, this approach can stub the private function in the class. The private
modifier usually adds more complexity for the testing, but not so much when you are using JS and Sinon.
export class RulesClient {
public send(rule: Rule): Promise<RuleResult> {
this.checkUser();
return ...;
}
private checkUser() {
...
}
}
This private function checkUser()
can be mocked by sinon as well. Use casting to type any, like 'checkUser' as any
or like <any> 'checkUser'
.
sinon.stub(RulesClient.prototype, 'checkUser' as any).callsFake(() => {})
And do not forget to restore the functionality of the prototype for other tests.
afterEach(async () => {
sinon.restore();
});
It’s definitely an awesome way to create stubs. In some other languages, you will have to create some adapter, in other words, some wrapper class to initialize the instance. And this class should be injected into the services. And most of the time it is added for the test’s purposes.
Photo by Max Duzij on Unsplash