I’ve recently been spending time researching into how to write unit tests for an existing codebase. One thing that keeps coming up in the research I’m doing is how important it is to write testable code from the start, but what is testable code?
Well from the research I’ve been doing I found that to write testable code a good approach is following the SOLID principles.
- S – Single Responsibility Principle
- O – Open/Close Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
So what do these principles mean and how do they apply to Angular?
Single Responsibility Principle is where an Object should do one thing and one thing only. For example in a controller or component you might have a function that loads some data, updates a local array then loads some more data from another source and finally displays this data in the UI. Well, that is four different things this one function is doing. To make this more testable it would be better to have one function that loads and returns data. One function that binds data passed into it to a local array, then another function which loads data from the second source and then finally allowing Angulars data binding to bind the data to the UI.
This means that you have only three separate functions, which can all be tested separately. One test to check the first function loads data correctly, a second test to check that the second function binds any data passed into it to the local array and finally the third test to check that the third function loads the data from the second source as expected.
Open/Close Principle is where a class is Open to extension, but Closed to changed. An example could be a function loads and returns some data. Then as the project continues the requirement for that data is changed as it is now ordered alphabetically. So you go back and amend the function to order the returned data after it has loaded. By doing this you’ve broken this principle. What you should really do it add a new function that calls the first function, then takes the returned data and then the second function performs the ordering of the returned data. This way you have two separate functions, both performing smaller tasks and the first function, which is already tested, has not been changed.
To write a function with this principle in mind, it needs to (in this loading data example) be able to return the loaded data and not just simply bind the returned data to a local variable or array. If the load data function returns the data as it loads it, then this allows other functions to take this returned data and extend what can be done with it.
Liskov Substitution Principle is where a class that uses another class, should be able to accept using another class without it knowing the difference. Within Angular, this means a controller or component may be using a service to load data, but the controller/component knows so little about the service that another service could replace the original one, without any changes needed to the controller/component.
This is an extremely important principle for writing testable code because adhering to this means that external services can be mocked in tests in order to write tests for just the function that uses this service. Through the use of mocking we can write tests that test different scenarios to see how our original function handles various responses from the mocked service.
Interface Segregation Principle this is important because it forces you to write smaller interfaces/classes which are easier to mock in tests. For example having a service that does loading data, aggregating data and ordering data, means that you will need to mock all three different functions in order to test something that is just using the loading data function. Separating it into three separate services means it is easy to mock just the one you need in order to test your code.
Dependency Inversion Principle is where your concretions (the act or process of concreting or becoming substantial; coalescence; solidification) or classes that are set out. Should only depend on classes that are abstractions of functionality. This means if you have a class that does something, like filtering an array. Then anything other classes it needs in order to perform this filtering should be an abstraction of the implementation. For example, with your class/function that filters an array, you may use something like Underscore or Lodash to help with the filtering. When you use Underscore or Lodash’s function you aren’t using the inner workings of the library, but an exposed function that abstracts away the inner workings of the library. So if in a new version of the library the helper function you are using is re-written to make it faster, as far as you are concerned you are still calling the helper function in the same way.
So by writing your code using this principle, you are writing more loosely coupled code, which is easier to test, because you test the individual parts. Parts that know nothing or need to know nothing about the inner workings of each other. This makes mocking for tests easier to do.
Following these principles will help make your code more modular, separated and therefore easier to mock for the tests you are going to write. From the research I’ve been doing the main concept to get in order to write more testable code is, in isolation, meaning writing code that can run in isolation is easier to test. It is easier to mock any dependencies (which there shouldn’t be many) it’s easier to write tests just for that single piece of code. Therefore writing more and more tests leads to an application which has more test coverage going forward.