Unit Testing is one of those aspects of a UI project that is often ignored. Not because nobody wants to do it, just that the cost of setup and maintenance can be taxing for a small team. Even on a large team, it can be ignored during those looming deadlines. Testing takes far greater discipline and rigor to do it right and to keep doing it right.
This post is not a panacea for Unit Testing but rather distilling the process of Unit Testing to a few key areas. Also, note that we are just focusing on Unit Testing and not on other kinds of testing like Integration (aka end-to-end), Acceptance, Stress/Chaos testing, etc. I’ll also limit myself to Web UI but the ideas are applicable to other UI platforms.
I’ll get it out right now and state it boldly:
Unit Testing is Hard
With familiarity, it will become easier, but it is still a multi-step process before you are in cruising mode.
1 Setup can be challenging:
On every project I worked on there was always a bit of fiddling with the settings and doing things differently based on the available infrastructure.
2 Devising a test for certain scenarios can be hard. This can require lot of mocking or changing the code to become more explicit about dependencies. This is probably the best part of testing but can be demotivating sometimes. The end result can be cleaner code and most likely results in better understanding.
3 Sometimes it makes more sense to do Test-Driven development
instead of Test-First
but requires more experience to know when to pick between the two.
A Test-First approach may not be as rewarding and has a longer lead-time to gratification.
Instead, seeing working code with manual testing can be more gratifying. Adding unit-tests at that point will also be more meaningful.
On a separate note, Test First Development
works great when doing API Design.
4 Once you are in the middle of the project, a breaking test can result in lot of investigation. This is part of the pain of software development but manifests more with a failing test! Of course, those same tests will help you later when doing serious refactoring.
The above is a decent real-world representation of what you have to go through. At least I’ve never had a silk road experience
yet :-)
Luckily things will get brighter. As you do more Unit Testing, you will start seeing the patterns emerge. After a few projects doing testing, you will realize the repetition that is happening. It will be the same kind of tests being done, possibly with different frameworks or libraries. The key is that the types of tests are quite limited. The following is a representative list of all the possible unit-tests you will ever write:
These tests are purely logic and have no UI involvement. Most likely these tests focus on the business-logic
of your application. For example, you may have a very specific way of parsing the JSON payload from a network request.
In this case, you will have a separate module, say parseEntity.js
that knows how to consume this payload
and make it usable on the client side.
The kind of tests you will write will include the following:
As you can see, its purely logic based and runs through a variety of situations to ensure the parsing module
produces the correct results. There is one clear advantage to doing Algorithmic tests, which is they can
all be done as data-driven tests. You can define a giant list of input
— output
cases and simply
run through them. You can even keep this list separately in a JSON file or even a Database! As you find
more edge-cases, you can craft a specific data-test, include in your list, and ensure your module works correctly.
Note that the inputs and outputs can be fairly complex. If you have a large set of input-output pairs, you can even store them externally, say in a JSON file, CSV or a NoSQL DB!
Operation = Math.pow(2, input)
| Input | Output |
|-------|--------|
| 1 | 2 |
| 2 | 4 |
| 3 | 8 |
Other examples of logic-based testing could be for:
• Mathematical Calculations
• Data transformations (such as map, sort, filter, group)
• Search algorithms, Regex
• Correct translations of strings based on locale
• Testing services used in the app (eg: persistence, preferences)
Technically this type of test falls under the Algorithmic category. However this use case is quite frequent on UI apps that it demands its own category. Here again you can have a data-table for all the input-output pairs. The types of things you will test include:
This category is probably the easiest to explain. These tests are for the UI components of your
application where you test the behavior and visual feedback. For example, if you had a SearchBar
component,
you would have tests such as:
Return
key fires the search callback. Same holds for clicking on the search button.These tests can also get very tricky for certain scenarios. For example, drag-and-drop is not an easy one. So is
testing for a combination of hot-keys and mouse/touch operations. For such tests, its probably best to wrap the core logic
in a service
and expect the service to change the internal state correctly. By reducing these user interactions to
some known, expected state, you can simplify your testing. It essentially becomes a state-based test at that point.
If you rather test this more explicitly, you can simulate events via jquery
and check if the callbacks are getting
fired. Of course, you also need to check if the correct state is being reflected via proper visual feedback.
These tests can also be treated as service-tests. Usually the network related activity is performed by the data-layer of your application, usually wrapped as a service. Making a real network request is not a responsibility of the Unit Test, neither is it feasible. This is where you will mock the network backend and ensure the proper call and parameters are being sent.
This test usually involves firing a service method and ensuring the proper network request is being made. You can also mock the response to return valid / error payloads and ensure the service layer behaves as expected.
If you have some functionality in your app where you are relying on setTimeout()
or setInterval()
, you have to do a time-based test.
However simulating or even waiting for the specific period is not feasible as it can slow down your tests. This is the case for
”Mock the Clock”! Yes, literally. Before you can run the test you have to hijack (normally via a library) the window.setTimeout()
and window.setInterval()
methods.
Your mock
will provide methods to advance the clock by the required time. This is the way to forward time in your test. At this point
you can check your behavior to see if it has performed the required set of operations.
Most UI code these days relies on async
/await
, Promises
or callback
-based asynchronicity. This requires some change in the way you test the functionality.
All testing libraries run synchronously, so they provide hooks for your test to signal back (with a done
callback) when it is ready.
Once you signal done
, the test will check the expectations and pass / fail the test.
Every unit test follows a standard 3-step process:
Libraries (Mocha, Jasmine) will provide you APIs for these 3 parts of testing. The popular libraries follow a Behavior-Driven approach. This means the API is more english-like and focuses on User-level behavior for the test cases.
describe
- used to create a test-suite or a group of test-casesit
- run a single test-casebefore
- called once to setup the context for the test suiteafter
- called after completion of the test suitebeforeEach
- called before each test caseafterEach
- called after completion of each test caseThe important thing to note in BDD frameworks is that you can nest the describe
and it
. This creates nested context so the inner-most
it
accumulates the state from all of the parent describe
.
To help you during the test execution part, there are few more helpers that you can use. These include:
Finally the aforementioned libraries also have APIs to assert your expectations for a test. These assertions will result in passing or failing the test. This is the last part of the test, where you check that the behavior was correctly performed and as per expectations.
There are also specialized libraries (Chai) that offer more fluent-APIs for performing assertions.
Irrespective of the kind of tests you write, there are some golden rules to adhere for all Unit Tests. Violating these rules will only make it difficult to scale your codebase as you add more features. They will also degrade your overall Developer Experience. So always strive to meet these rules!
Testing is hard and takes experience to get it right. By following the above principles and remembering that: “there are only a limited types of tests”, you can keep the pain of testing to a minimum.
Next Step: M
ake T
esting F
un (MTF) :-)