Getting in the habit of Test Driven
August 14, 2018
With Test Driven Development (TDD) not only do you not have to worry about that false high and come down when you realize you have to go back and add tests, but you can ensure you have full test coverage as you can define the tests to cover edge cases from the start. This way, when all the tests pass, you know the edge cases (undefined / null / invalid values) are covered.
An example I will walk through is in recently adding a 30 day trial license to https://account.n.io/signup. The trial needs to cut the user off when the trial_ends_at date has past. We will need to write logic to see if the trial has ended or not.
The scenarios we want to cover are:
- returning true if trial_ends_at in past (the only scenario that will cut off access)
- returns false if valid license (no trial_ends_at date set)
- returns false if trial_ends_at in future
- returns false if no license (our null edge case to make sure the function does not error)
We have additional functionality around licensing, so not only is there a trial_ends_at date but also an ends_at date which cuts off access to the app completely and acts differently than a trial end date.
Given this we will also need the following scenarios:
- returning false if ends_at in past (not trial related)
- returns false if ends_at in future
Given these scenarios we can write out our tests using mock data. The data is imported from a mocks file which is used throughout all tests so that data doesn’t have to be mocked in each individual tests. This also, IMHO, makes the tests much easier to read than having to define a mock data set in each test.
import { none as noLicenseMock, valid as validLicenseMock, validEnd as validEndLicenseMock, invalidEnd as invalidEndLicenseMock, validTrial as validTrialLicenseMock, invalidTrial as invalidTrialLicenseMock, } from 'tests/mocks'; import { hasTrialEnded } from './'; describe('hasTrialEnded', () => { test('returns false if no license', () => { expect(hasTrialEnded(noLicenseMock)).toEqual(false); }); test('returns false if valid license', () => { expect(hasTrialEnded(validLicenseMock)).toEqual(false); }); test('returns false if ends_at in future', () => { expect(hasTrialEnded(validEndLicenseMock)).toEqual(false); }); test('returns false if ends_at in past', () => { expect(hasTrialEnded(invalidEndLicenseMock)).toEqual(false); }); test('returns false if trial_ends_at in future', () => { expect(hasTrialEnded(validTrialLicenseMock)).toEqual(false); }); test('returns true if trial_ends_at in past', () => { expect(hasTrialEnded(invalidTrialLicenseMock)).toEqual(true); }); });
Now that our tests are written, we can write the initial hasTrialEnded function. The basic logic is to check if the trial_ends_at date is in the past.
export const hasTrialEnded = (license) => { const trialEndsAt = license.get('trial_ends_at'); const now = new Date(); return (new Date(trialEndsAt)).getTime() <= now.getTime(); };
Just by looking at the code, this looks good. It compares the date of the trial with the current date. If we were not doing Test Driven Development, this may get pushed up and possibly merged in. However, once we run our tests on this function we can see the majority are failing:
So what went wrong? It looks like our tests have an edge case where the trial_ends_at date is undefined or null. We didn’t consider this when writing the function and may not have caught it by running a manual test as the data set we are using may always be on a user with a trial. But we also can have enterprise users that are not on a trial and thus do not have an end date. That is where this logic would have failed.
We can update the function to add a truthy check on the date:
export const hasTrialEnded = (license) => { const trialEndsAt = license.get('trial_ends_at'); const now = new Date(); return !!trialEndsAt && ((new Date(trialEndsAt)).getTime() <= now.getTime()); };
After adding the truthy check we see:
