DEV Community

Xavier Le Cunff
Xavier Le Cunff

Posted on

Setup test driven development on any React app with Cortex

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

What You'll Learn

  • Create a service
  • Test a service with Jest
  • Code any feature with test driven development (TDD)

What you'll need

  • Setup Cortex in
  • Setup Jest with Typescript on your project

This will be done with a simple counter to be more focused on test driven development.

You can access to th code in Code Sandbox
Edit frosty-surf-4kp6v2

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);
  })
});
Enter fullscreen mode Exit fullscreen mode

:::info

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode
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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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').increment();
    expect(core.store.counter.count.get()).toBe(-1);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)