Write Your Test Code First — Part 1: Basics

Update: In the previous iteration of this post, the addFieldService was a function instead of an object, it has been fixed.

The Dirty Word of Development

Testing has widely become that thing in web development (and maybe the industry as a whole) that is heralded as important but hardly done. Perhaps a lot of people are and just aren’t talking about it — hopefully, that is the case.

Why should developers be writing tests? Because despite the fact that it seems like “a waste of time” or something developers “can’t be bothered with” as many voices have yelled over and over again, testing saves developers time in the long run.

This post will walk through an example and show how writing the unit tests first do a lot for the developer, including:

  • Cleaner code
  • Less errors
  • Smaller production code

A Shaky Foundation

There’s a brand new project handed down by The Decision Makers in the company that has one real requirement: an application that can create a table-based form with nested grouping (designers and mobile-first advocates everywhere just shrieked in horror). Here’s a mock-up of what that might look like:

I Call It “A Mobile-First Horror Show”

Honestly, what the UI looks like is irrelevant to this discussion, but an idea of what the product would look like should be a helpful reference.

A mock-up and a vague requirement are all there is from which to work. One of the complaints out there is “unit tests can’t be written first if there aren’t solid requirements!” The unit tests aren’t one-to-one of requirements and, most times, they are written at a finer level of detail than a requirement.

Starting from Nothingness

First the developer takes some time to think, Okay, what the heck is needed to do to accomplish this? After an hour or two of contemplation (slash browsing the Internet) there are some very basic concepts to go on:

  1. There is a list of fields
  2. A field can have child fields
  3. A field has a type
  4. A field has a label
  5. A field needs a way of being uniquely identified
     While this may not be obvious at first, but a way of referencing fields to each other is necessary, particularly in relation to the grouping requirement.
  6. There needs to be a way of adding fields to the form

Items #1–5 are data structure concepts and item #6 is actual functionality.

Defining the Data Structures

Data structures are the very foundation of what a code base will become and is, so they are important to get right. That doesn’t mean they will be right on the first try, but if something isn’t working because of a sloppy or ill-designed data structure, it should be considered that adjusting the data structure might be needed to relieve the pain instead of patching the same painful code over and over.

The first data structure requirement is that there is a “list of fields”. This may be obvious, but that indicates the need for an array. Boom, that’s one done!

Concepts #2–5 are about the data structure of an individual field, just based on those ideas alone the following model is derived:

let field = {
label: '',
name: '', // this is the unique identifier, needed for grouping
parentField: '',
type: ''
}

Note that some ES2015/ES6 features (like let in this instance) will be utilized in this post. Also note that the relationships will be from a field to its parent as opposed from a parent to its children. The reasoning is that if fields can be reordered in the table, at least two structures (the field being moved and it’s parent’s list of children) would need to be updated where with this relationship, the parentField value does not need to be modified unless a field’s parent changes its name.

Woo-hoo! Five of the six concepts have been tackled! But no testing has been accomplished yet! As was stated at the start of this section, designing the data structures ahead of time lays a strong foundation for how the code is approached and, consequently, the tests.

Unit Test Skeleton

Remember, the primary concept being worked off of is that “there needs to be a way of adding fields to the form.” Start off at the most simple concept, just adding a field to an array. That’s almost as simple as they come. And that’s the first test:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
});
});

Okay, so what’s that mean?

describe('Service: addFieldService', ()  => {
// ...
});

This is using Jasmine for tests, and describe is a Jasmine-provided function. Think of it as a way of logically grouping tests — in this case the addFieldService tests are being grouped together. The first parameter is just a descriptive string for clarity (both while developing and test output), the “Service: “ prefix is just a personal preference on how to label tests. The second parameter is the function that Jasmine will run that contains calls to run tests. Speaking of which…

it('adds a field to the list of fields', () => {
});

This is another Jasmine-provided function, it runs a “spec” which is just a single test. That first parameter, again, is a narrative for clarity and the second is the function for Jasmine to run, that will contain the code for a test. Obviously, at this point, there is no test code.

The Full First Unit Test

Exactly what is needed to test is known: that when the addFieldService is asked to add a field, it adds it to the list of fields. Be aware that the following application code will be using pure functions. Here’s the full first test code:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
let fields = addFieldService.addField('string', []);

expect(fields).toEqual(['string']);
});
});

The testing at its core is very simple: run a function (or method) then test the output against what is expected. Note, again, that expect and .toEqual are Jasmine-provided functions/methods.

So in terms of writing tests before application code, what does this mean? The tests were written to fit the identified concepts and the data structures. The following have been defined by these tests:

  1. The addFieldService.addField method takes two parameters, a string and an array.
  2. The returned value from addFieldService.addField is an array containing the second parameter of the call plus the new field.

Now the entire point of writing tests first is so that the application code will contain the absolute minimum amount of code necessary to pass the test. Remember this, remember this so much. Tests first will almost always lead to less code. And that’s a great benefit because the application code becomes sufficiently more manageable.

Initially the field data structure will not be utilized. That’s because this isn’t the final test code, just the first iteration. After this test is passing, the field data structure will be used and then more iterations will be performed to fill in the necessary attributes of the data structure.

The “Normal” Unit Testing Cycle

As perscribed by “Uncle Bob” Martin, the testing process is a red-green cycle. Here are the steps:

  1. Write unit test code.
  2. Run the test to failure (this is the “red”).
     This is expected because no application code has been written to pass the test.
  3. Write just enough code to pass the test.
  4. Run the test to success (this is the “green”).
  5. Either repeat for the next test or iterate the current test then return to #2.

At this point it is not necessary to go into how to run the unit tests since it is out of the scope of this post.

Finally Some Application Code!

If run, the tests would now be complaining that addField is trying to be called on an undefined object, which makes sense since that object is the addFieldService, which hasn’t been defined. As a first step to passing the tests, a service named addFieldService should be defined (it will be placed on the global scope for simplicity, but it’s always better to namespace it in the real world to avoid collisions):

const addFieldService = {
};

Okay, looking good! At this point, the tests would now be complaining that undefined is not a function. This is because an .addField method is missing from the addFieldService:

const addFieldService = {
addField: () => {
};
};

Now, there’s no complaints that the functions are undefined, but the tests would be complaining that the value returned is undefined and that it does not match the expected output. From the tests its clear that an array must be returned:

const addFieldService = {
addField: () => {
return [];
};
};

Well, at least there’s a return value! But still, it doesn’t match the expected output of [‘string’]. To get that return value:

const addFieldService = {
addField: () => {
return ['string'];
};
};

And there’s passing tests! There’s the green step in the red-green cycle! But, there’s something brittle (meaning it’s too rigid) about this test. What if a user wants to create a decimal field? What then? Well, the test code can be amended to support that:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
let fields = addFieldService.addField('string', []);

expect(fields).toEqual(['string']);

fields = addFieldService.addField('decimal', fields);

expect(fields).toEqual(['string', 'decimal']);
});
});

Please note that obtuseness of all this is intentional and it’s not necessary to be this deliberate about iterating nor write code in such a brittle way.

A test failure will now occur because the second expectation — that a second call to addFieldService.addField would yield the array [‘string’, ‘decimal’] — cannot be met since only [‘string’] is being returned. Those tests must be satisfied! The tests make it pretty clear that the value passed in as the first parameter should be appended to the array passed in the second parameter and the new value returned.

const addFieldService = {
addField: (type, currentFields) => {
let newFields = currentFields.slice(); // this is just a way of quickly copying an array
// we'll keep using it even though it doesn't make a deep copy
newFields.push(type);

return newFields;
};
};

And, BAM, now there’s a fully successful test.

Utilizing the Correct Data Structure

As the last iterations of this test (and post), the field data structure defined earlier will be used to get the real list of fields going. At this point, with no groups, there are 3 attributes of a field that need to be set:

  1. Label
  2. Name (which needs to be unique)
  3. Type

One-by-one each attribute will be addressed to achieve this. First, the type will be set, making the updated unit test:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
let fields = addFieldService.addField('string', []);

expect(fields).toEqual([
{
label: '',
name: '',
parentField: '',
type: 'string'
}
]);

fields = addFieldService.addField('decimal', fields);

expect(fields).toEqual([
{
label: '',
name: '',
parentField: '',
type: 'string'
},
{
label: '',
name: '',
parentField: '',
type: 'decimal'
}
]);
});
});

And, once again, the tests would fail because the expected output and the actual output do not equal each other. To fix this, the application code will be updated to create a new field and set its type attribute to the first parameter of an addFieldService.addField call.

const addFieldService = {
addField: (type, currentFields) => {
let newFields = currentFields.slice();

newFields.push({
label: '',
name: '',
parentField: '',
type: type
});

return newFields;
};
};

Viola! The unit tests are back to green. Next each new field’s label attribute needs to be updated. The unit tests become:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
let fields = addFieldService.addField('string', []);

expect(fields).toEqual([
{
label: 'String',
name: '',
parentField: '',
type: 'string'
}
]);

fields = addFieldService.addField('decimal', fields);

expect(fields).toEqual([
{
label: 'String',
name: '',
parentField: '',
type: 'string'
},
{
label: 'Decimal',
name: '',
parentField: '',
type: 'decimal'
}
]);
});
});

Causing yet another “red” step in the unit testing cycle. It looks like the label is just a capitalized version of the type, which can be accomplished by calling .toUpperCase() on the first character of type and concatenating that with the remaining string.

const addFieldService = {
addField: (type, currentFields) => {
let newFields = currentFields.slice();

newFields.push({
label: type[0].toUpperCase() + type.slice(1),
name: '',
parentField: '',
type: type
});

return newFields;
};
};

The tests should now be green again. And, finally, the most difficult portion of adding a field, naming the field a unique name. A simple way of doing this would be to just append the type with a the number of fields of that type plus one, which is how it will be handled. This will not guarantee uniqueness, but that is not the aim of this test. In a future post, a test will be created to address the uniqueness concern. Here’s the final test code:

describe('Service: addField', () => {
it('adds a field to the list of fields', () => {
let fields = addFieldService.addField('string', []);

expect(fields).toEqual([
{
label: 'String',
name: 'string1',
parentField: '',
type: 'string'
}
]);

fields = addFieldService.addField('decimal', fields);

expect(fields).toEqual([
{
label: 'String',
name: '',
parentField: '',
type: 'string'
},
{
label: 'Decimal',
name: 'decimal1',
parentField: '',
type: 'decimal'
}
]);
});
});

This will fail the tests again. The easiest way to pass the test would be to look for all fields in the currentFields array that have the provided type, then append that number plus one to the type and set the name attribute to the value. That application code would be:

const addFieldService = {
addField: (type, currentFields) => {
let newFields = currentFields.slice();
const fieldsWithType = currentFields.filter(field => field.type === type);

newFields.push({
label: type[0].toUpperCase() + type.slice(1),
name: type + (fieldsWithType + 1),
parentField: '',
type: type
});

return newFields;
};
};

Once, again, the test is passing. And just like that, a method has been created with the bare minimum code to start adding fields to a list!

Reaping the Benefits

One of the biggest benefits of a unit-tested application is that refactoring the application code has a built-in check to make sure nothing has been broken. For instance, if the creation of a field was separated into its own function, the code would be refactored to the following:

const addFieldService = {
addField: (type, currentFields) => {
let newFields = currentFields.slice();
const field = createField(type, currentFields);

newFields.push(field);

return newFields;
};
};

const createField = (type, currentFields) => {
const fieldsWithType = currentFields.filter(field => field.type === type);

return {
label: type, // breaks unit test
name: type + fieldsWithType, // breaks unit test
parentField: '',
type: type
};
};

When the tests are run, the two lines marked with “breaks unit test” comments would cause the expect outputs and actual outputs to not match up, letting the developer know that refactoring broke something. After investigation, the code would be updated and, once the tests pass, the refactored code could be used confidently.

Until Next Time

Hopefully it’s clearer that writing the tests first and not just jumping into implementation allows the application code to stay concise and clean. Additionally (albeit more importantly), it provides a way of guaranteeing that code behaves in the expected manner because its been coded to an expectation.

Next time, how to run the tests will be looked at, utilizing Node.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.