Your First Test
Write unit tests for a plain service: the shape of a test, the matchers that say what you expect, and the bugs a test is worth nothing without catching.
What you'll learn
- Structure a test with describe, it and beforeEach, and read its result
- Choose between toBe, toEqual, toHaveLength and toThrow, and know why toThrow takes a function
- Judge a test by the bugs it catches, not by whether it passes
Every lesson so far was graded by calling your endpoints. That is a test: something runs your code, looks at what came back, and says pass or fail. This course hands you the tool that did it. A test is code that exercises other code and states what should have happened, and the point of writing one is not the green tick. It is that the next change, yours or a colleague's, cannot break what the test states without someone hearing about it.
The shape
A test file, by convention something.spec.ts next to something.ts, is a script that registers tests. Three functions do the registering, and they exist as globals, no import needed:
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
it('adds two numbers', () => {
const sum = calculator.add(2, 3);
expect(sum).toBe(5);
});
});
describe groups tests under a name, usually the class under test. it (or test, the same function) registers one test: a name that reads as a sentence, and a function that runs it. beforeEach runs before every test in the group, and it exists so each test starts from a fresh object and cannot depend on what an earlier test did. A test that only passes when another ran first is a test that will fail on the day it runs alone.
Inside a test, the three lines you see are the shape every test has: arrange the situation, act on the code under test, assert what came out. Keep them in that order and a failing test reads like a bug report.
Saying what you expect
expect(value) returns an object whose methods are matchers. The ones you will use every day:
| Matcher | Passes when |
|---|---|
toBe(x) | the value is x, by Object.is: the same number, string, boolean, or the same object reference |
toEqual(x) | the value has the same shape and contents as x, however deep |
toHaveLength(n) | an array or string has that length |
toContain(x) | an array contains x, or a string contains the substring |
toBeDefined(), toBeUndefined(), toBeNull(), toBeTruthy() | what they say |
toThrow(x?) | calling the value throws; with x, the error is that class, or its message contains that string |
.not in front of any matcher inverts it: expect(luna.id).not.toBe(tom.id).
The one that trips everyone is toBe on objects. Two objects with the same contents are not the same object, so expect(service.findOne(1)).toBe({ id: 1, name: 'Tom' }) fails even when the cat is right. Use toEqual for objects and arrays, toBe for primitives.
The other is toThrow. Writing expect(service.findOne(99)).toThrow() calls findOne(99) before expect ever runs, and the throw escapes the test. Hand expect a function that does the call, and the matcher calls it inside a try:
expect(() => service.findOne(99)).toThrow(NotFoundException);
What a failure looks like
When a matcher fails, the test stops there and the failure reads:
expect(received).toEqual({ id: 1, name: "Tom", age: 3 })
Expected: { id: 1, name: "Tom", age: 3 }
Received: { id: 1, name: "Tom", age: 4 }
at cats.service.spec.ts:14:17
Read it top down: which matcher, what you said should happen, what did, and the line in your spec. A good test name plus that message is usually enough to find the bug without a debugger.
A test is worth the bugs it catches
it('creates a cat', () => { service.create('Tom', 3); }) passes. It also passes if create stores nothing, returns nothing, or stores the wrong cat, because it states nothing. A passing test proves only that no assertion failed, and a test with no assertion cannot fail.
This workspace grades your tests the way the harness behind this course grades every lesson: it runs them against the service, where they must pass, and then against several buggy versions of the service, where at least one of them must fail. The Tests tab lists those bugs. A bug that gets through is a hole in your tests, and the tab tells you which. This is called mutation testing, and it is the honest measure of a test: not "does it pass", but "would it notice".
Your task
cats/cats.service.ts is the service from Basics, with nothing to inject, so new CatsService() is all a test needs, and beforeEach already does it. Replace the four it.todo entries in cats.service.spec.ts with tests:
create()returns the cat with the name and age it was given, and two cats get different ids.findAll()lists the created cats in the order they were created.findOne()throwsNotFoundExceptionfor an id nobody has.remove()returns the removed cat, and afterwards the cat is neither listed nor found.
Then look at the bugs the tab says your tests must catch, and make sure each one fails at least one test.
When it fails
Expected the promise to resolveorneeds a function:toThrowwas given a value instead of a function. Wrap the call in() => ....toBefails on two cats that look identical: they are two objects.toEqualcompares contents.- A bug escapes: no test states the thing the bug breaks. "every cat gets id 1" escapes tests that never compare two ids; "remove keeps the cat" escapes tests that check only the return value.
- A test passes alone and fails with the others: state leaks between tests.
beforeEachshould build the service fresh.
Remember
describegroups,itregisters,beforeEachresets; arrange, act, assert.toEqualfor objects and arrays,toBefor primitives,toThrowon a function.- A test that cannot fail states nothing; judge a test by the bugs it would catch.
Stuck? Show a hint
Each it() arranges (create some cats), acts (call the method), asserts (expect). Use toEqual for objects and arrays, toBe for numbers and strings. To assert a throw, hand expect a function: expect(() => service.findOne(99)).toThrow(NotFoundException). For remove(), check the return value and then that findAll() and findOne() no longer know the cat.