Counter
Test Driven Development is a a concept quite easy to understand in theory but hard to practice, even more in front end development. It requires a good architecture for testing the use cases and not the implementations, otherwise we would change the tests each time we change the code and make the tests almost impossible to maintain
- Create a service
- Test a service with Jest
- Code any feature with test driven development (TDD)
- Setup Jest with Typescript on your project
This will be done with a simple counter to be more focused on test driven development.
Introduction to TDD
Test-Driven Development (TDD) is a software development process that relies on a short, repetitive development cycle
It's a method where you write a test for a piece of functionality before you write the code to implement it
Test driven development is made of 3 steps:
- Add a test
Before you write the functional code, you write a test for the new functionality. This test will initially fail, as the functionality has not been implemented yet.
- Write the code
Write the minimal amount of code necessary to make the test pass. This often means the code is not perfect and might need refactoring later.
- Refactor
Look at your code and consider if it can be cleaned up. Refactoring is about making the code cleaner and more maintainable without changing its functionality. You can refactor with confidence because you know you have tests that will alert you if you break something.
Counter example
Increment
Add a test
import { Core } from '../cortex/_core';
describe('counter', () => {
it('should be instantiate with 0', () => {
const core = new Core();
expect(core.store.counter.count.get()).toBe(0);
})
});
The Core
is the same that is injected in the CortexProvider
in main.tsx
so we can test it independently from React and be sure that the changes will be reflected on the UI
We expect first that the initialState of the counter will be 0, The test cannot run because the service doesn't exist yet
Write the service
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
}
After running the test, it should succeed
Testing the increment
We want to create a method that will increment the counter when called
Let's write it empty and follow by writing the test, so the test can run and fail
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
increment() {}
}
import { Core } from '../cortex/_core';
describe('counter', () => {
it('should be incremented', () => {
const core = new Core();
core.getService('counter').increment();
expect(core.store.counter.count.get()).toBe(1);
});
});
After running this test it should fail because the counter stays at 0
Let's write the minimum code to make it work
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
increment() {
const count = this.state.count.get()
this.state.count.set(count + 1);
}
}
Refactor
There is not much to refactor in this simple example, but if we look closer to the the legend app state lib, we can see that set
can take a callback with the previous state as parameter
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
increment() {
this.state.count.set((count) => count + 1);
}
}
If we re-run the test, everything should pass like previously
Decrement
Write the test
We can do the same with decrement:
import { Core } from '../cortex/_core';
describe('counter', () => {
it('should be incremented', () => {
const core = new Core();
expect(core.store.counter.count.get()).toBe(0);
core.getService('counter').increment();
expect(core.store.counter.count.get()).toBe(1);
});
it('should be decremented', () => {
const core = new Core();
expect(core.store.counter.count.get()).toBe(0);
core.getService('counter').decrement();
expect(core.store.counter.count.get()).toBe(-1);
});
});
The test doesn't pass as expected
Write the code
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
increment() {
this.state.count.set((count) => count + 1);
}
decrement() {
this.state.count.set((count) => count - 1);
}
}
The test should pass now
A new use case
Modify the tests
Now we want the counter not to be decremented under 0
Our last only decrement test is not working for our use case since it tests if the counter goes under 0, we have to re-write it. So we instantiate the counter at 5 and expect it to be at 4 after calling the decrement service
it('should be decremented', () => {
const core = new Core();
core.store.counter.count.set(5)
core.getService('counter').decrement();
expect(core.store.counter.count.get()).toBe(4);
});
We can now write the second test
it('should not be decremented under 0', () => {
const core = new Core();
core.getService('counter').decrement();
expect(core.store.counter.count.get()).toBe(0);
});
Here the counter is at its initial state 0 and should stay at 0 if it is decremented
Write the code
type State = {
count: number;
};
export class CounterService extends Service<State> {
static initialState: State = {
count: 0,
};
increment() {
this.state.count.set((count) => count + 1);
}
decrement() {
if (this.state.count.get() !== 0) {
this.state.count.set((count) => count - 1);
}
}
}
Conclusion
We learned to use TDD to append new features If we want any other feature, we just have to write an explicit test before writing its code