Test driven development TDD best practices and notes

I was asked to discuss test driven development with our teams and did a bunch of research to share with everyone. Below is what I presented.

Focusing on unit tests, other layers of TDD is:
– regression tests (when things are changed old bugs don’t come back)
– integrations tests (multiple steps, a flow of logic)
– e2e tests (a user journey flow from start to finish)

Research the layers of tdd you could find useful tests to help add to a project’s stability.

The argument for routine

Make TDD a part of your coding routine. The initial setup of the tests might take some TLC and time, but once that is done you should rarely have to do much new development. Once TDD becomes habit it becomes easier to to do.

The below is a combination of online research, experience and advice given by Robert C. Martin in Clean Code.

The 3 laws of TDD:

1. You have to write a failing unit test before you write any code.
2. You must not write more than was is sufficient to make the test fail.
3. You must not write more production code than what is sufficient to make the test pass.

Structure of a test:

Build, Operate, Check or “Arrange, Act, Assert,” (sometimes there’s a 4th step – teardown – cleans after test ends – most frameworks should handle that for you)

1. Build / Arrange

What data do you need? What stubs should you call in? On backend you might want to create a fake model to use.

2. Operate / Act

Usually: Call a method. Or pass values into a method.

3. Check / Assert

Check the result of the Operate / Act step. Is it what you expected?

Make sure the tests are clean

Make sure you give as much thought to the quality of your tests as you do for the quality of your production code. If you let your tests rot, your code will rot.

Clean tests = readability; clarity, simplicity, density.

Guidelines:

One assert per test. Same way that in your code you look at DRY and single responsibility – if you have to assert multiple things in a test relook your code, you are probably doing too much in the actual code of method you are testing.

Test a single assertion.

FIRST

Clean tests follow with 5 FIRST rules, as mentioned in Clean Code:

Fast

– runs quickly, you don’t want to wait 10 minutes for the tests to run, no one will run the tests. The ideal situation is that the tests run continuously while you are coding.

Independent

– one test should not rely on another test running

Repeatable

– You should be able to run tests in staging, prepared, prod. You should not hard code tests to a specific environment.

Self-Validating

– No manual interpretation needed (human eyes don’t define if its passing or not)

Timely

– Should be written before your code.

Avoid

Be aware of these and avoid them

Tightly coupled tests = harder to refactor code.
Tests that depend on other tests to run.
Too many assertions in one test.
Testing other parts of the code that are outside of the method you are testing.

Remember: Test rot = code rot. Keep tests clean.

TDD should be easy and quick. The first warning sign that you are doing incorrect tests and not following best practice is if they take a lot of time.

Goal of TDD

Write tests that “have your back”.
Tests should be quick and easy.
Forces you to write neater code. Code quality goes up. Bugs go down. Rewrites become less, refactoring over rewrites.

Javacript: More functional, less object oriented

A colleague pointed out that when I program in javascript I’m trying to force object oriented programming onto javascript. Rather than keeping with the javascript style of programming. The main problem is I rely on the “new” keyword in my javascript code.

The scenario:

I want a base object, DataInterface, that can handle api methods and data.

I have reusable methods that are interfaced, and then I want to be able to extend “DataInterface” and have the endpoint updated for each instance of the class.

The ideas:

It can be further debated whether this is the right solution for the scenario, but below is the outcome of the discussion:

DataInterface class with methods:

Eg:

1
2
3
4
var DataInterface = function() {
    this.getAll : function() {}; //just an example
    this.getByField : function(field, value) {}; //just an example
}

And this base object has an api endpoint attribute

Eg:

1
2
3
var DataInterface = function() {
    this.endpoint = ‘/endpoint’;
}

Then I want to extend off of this object and have specific data sets,

Eg

1
2
Products.endpoint = ‘one’;
Leads.endpoint = ‘two’;

And then Products and Leads can have additional extended methods, but they inherit the same original behavior from the base class.

The focus:

I am going to focus on just updating the endpoint for two instances of DataInterface, and exclude any methods:

What I originally wrote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var DataInterface = function() {
    var endpoint;
    this.setEndpoint = function(_endpoint) {
        endpoint = _endpoint;
    };
    this.getEndpoint = function() {
        return endpoint;
    };
    return this;
}

var ProposalService = new DataInterface();
var ProductService = new DataInterface();
ProposalService.setEndpoint('one');
ProductService.setEndpoint('two');

console.log(ProposalService.getEndpoint());
console.log(ProductService.getEndpoint());

What this will output is ‘one’, and then ‘two’ in the console. Two instances of the DataInterface has been created with two unique endpoints.

Out with the new

However, if you remove the “new” keyword, which would make it more “javascript-like”:

Using the same class, but removing “new”.

1
2
3
4
5
6
var ProposalService = DataInterface();
var ProductService = DataInterface();
ProposalService.setEndpoint('one');
ProductService.setEndpoint('two');
console.log(ProposalService.getEndpoint());
console.log(ProductService.getEndpoint());

What will happen now is the console will print out ‘two’, ‘two’. Instead of two instances, we have the same instance being pointed to by 2 variables. So the endpoint is not unique.

In comes the factory pattern

So if we rewrite the code a bit, and have DataInterface as a factory pattern that returns an object, then we can achieve the same result but without the new keyword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var DataInterface = function() {
    return {
        endpoint : '',
        setEndpoint : function(_endpoint) {
            this.endpoint = _endpoint;
        },
        getEndpoint : function() {
            return this.endpoint;
        }
    };
};

var ProposalService = DataInterface();
var ProductService = DataInterface();
ProposalService.setEndpoint('one');
ProductService.setEndpoint('two');
console.log(ProposalService.getEndpoint());
console.log(ProductService.getEndpoint());

You will now get the output of ‘one’ and ‘two’ in the console, and we did not rely on the “new” keyword.

Just a handy little snippet of code to remember how to not use “new” in javascript when creating instances.

Be a better developer by thinking of many solutions

I try challenge my code on a regular bases. I have noticed my code get better the more I pause and question if it definitely is the best solution.

The goal is to solve a problem

I have a lot of designers for friends and what I have noticed is how designers approach their projects.

Brain storm

Designers tackle projects by brainstorming. They go sit and think about lots (and lots and lots) of creative ideas. They want a big collection of ideas, and then they want to take the top 3 or 5 (or 10 even) from that list.

As developers we should try this as well.

Brain storm naming conventions, folder structures, reusable services, tests, constants, best practices, useful abstractions etc. Will a new developer understand the code you have written? Is the code understandable without comments?

There are many aspects to the code you write that if overlooked could incur technical debt. Think about a big picture solution.

Have goals

When you start your code, decide what you need to achieve as a holistic solution. Certain goals should come with any final solution:

  • Is the code easy to test? Do the tests help make sure the code keeps working? (This is for developers who partake in the TDD approach).
  • Is the code self documenting? Is it easy to hand over to a new developer without any teaching required?
  • Can the code be altered easily?
  • Is the naming conventions consistent with the rest of the project?
  • Is the file and folder structure consistent and intuitive? Will other developers have to go on an Easter egg hunt to find the right file?
  • Can design patterns like SOLID help make the code neater?
  • Are your methods doing too much? Can you separate the code further?
  • Are best practices being used?

The list can go on and on. You should always keep these types of questions in your mind, and think of new ones to push your skills.

Beware the problem dressed as a solution

The first solution is not always the best solution. And be skeptical if you can only think of one solution. Our brains can be lazy. Ever have that situation where the easiest way to solve a problem becomes the most problematic to maintain?

A solution should not come at a high maintenance cost. Solutions should not be short term. Else you have just created another problem. Might not be a problem now, but it will be a problem eventually.

Will the real slim shady please stand up

Take the time to brainstorm and think of many ways to solve the problem you are facing. Have enough foresight to see which solution is the real long term solution.

Avoid technical debt. It will whittle away at your project and slowly destroy your passion for what you do.

Also keep in mind the team you work with. Designers, developers, strategists, project managers, business analysts, etc. Do not code something at the expense of the team’s efforts. Always remember to communicate often.

Code solutions, not problems. Word.

The zen of python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one– and preferably only one –obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!