Clean Up Your Frontend Tests: Part 2

Tips & Tricks

Paweł Nowakowski
TechTalks@Vattenfall
6 min readNov 30, 2020

--

This article is a part of a cycle. All parts are:

  1. Why we focus on integration tests for UI components.
  2. Tips & Tricks
  3. Reusable testing modules.
  4. Using Page Objects to manage complexity.

Credits to Stawiarski Jakub, co-creator of the series.

As a team, we write a lot of tests. We failed many times and wrote a crappy, unreadable code. But we have learned from our mistakes and improved the process of writing specs. While enhancing our approach we decided to collect lessons learned and share them with you.

We strongly emphasize the value of writing tests before implementation. And, actually, this is the first tip: do use the Test-Driven Development process. There is enough literature that describes the advantages of TDD, so we are not going to elaborate more on this. We encourage you to study it if you have not done it already. We understand that TDD is not easy, especially when you are not fully familiar with the language/framework you use. But still, give it a try, and you’ll see that your skills improve.

OK, let’s put TDD aside and focus on the specific tips and tricks which may help you to write your perfect tests. NOTE: Angular is our main technology while developing, so the below examples are based on the TestBed, Jasmine and Karma.

Example

Have you ever felt that your assertions are not quite readable and are too long? Yes, we too. Before we go to the point, let’s take a look at an example first:

As you can see this is a very simple component responsible for storing numeric values (greater than 9) in local storage. So, let’s test it:

Avoiding the random failures

You may notice that we didn’t mock local storage and that can be unacceptable for many developers. We agree, but it’s not the point of this article, so we skip it. What is worth to mention is that if you use real storage in the tests, you should remember to clean it up after each spec:

If you won’t do that your tests may fail randomly because they can influence each other. Even worse, they can be interrupted by other tests from different spec files. So, whatever you do, mocking or not, remember to clean up after yourself to avoid hours of investigating why your tests are failing.

Named debugElement references

OK, as you can see we have some tests which cover storing the value in the local storage. The problem is that we’re breaking the DRY principle a bit.

There are some repetitions that could be eliminated (like querying input many times). So, instead of this:

we can have this:

Remember that in our case we have three such describe sections. It could be improved in a more efficient way - make it reusable:

Our expect can be improved as well because localStorage.getItem(...) is used many times and is a bit long which breaks the readability of the assertion (we like to read them as a whole, neat sentence). So let’s extract it as a reusable function too:

Here you have all together after refactoring:

The first impression would be: ‘Hmm, but there is more code than before…’.
Yes, if we take into consideration only one describe. The more there are the more useful these helper functions are. Another thing is readability. For us, tests are a kind of documentation. If we needed to go back to the code which was written half a year ago and has complex logic, then assertions would be the first we read when trying to understand "what's going on in here?".

When you read the describe above you may realize that you immediately know what kind of actions beforeEach performs even if you’re not familiar with the insides. The same situation occurs with it. All credits go to properly named functions that are easy to read and understand.

You may also notice that we use beforeEach a lot. Yes, and it's intended behaviour. Instead of repeating the simulation of user actions in it and disturbing its understanding by the programmer, we extract actions to beforeEach to keep it clean and simple.

Data-driven testing

Data-driven testing is a software methodology that may be very helpful when you write a lot of tests based on the provided data. In simple words: we create an array of test cases where we store information like data provided by the user, expected results, and additional stuff used in the tests. Such an approach gives us flexibility when managing tests and makes covering edge cases simpler. In our example with the value stored in the local storage each case is represented by a separated describe. This means that adding every new test case requires writing quite a bit of boilerplate code like describe, beforeEach and it. It sometimes leads to the situation that you might resign from writing more tests because of the number of lines in the file 💩. Let's see how it would look like with DDT implemented, starting from declaring test cases:

Adding or removing each case is pretty straightforward, it’s just a simple configurable object. We not only have covered previous cases but easily appended more to check. A great advantage of DDT is that your describe will probably stay in the same shape independently of the test cases:

Remember to construct the text in the describe so that karma doesn’t merge it. It will improve readability and help you when debugging.

DDT perfectly fits other test cases like:

  • forms,
  • validation messages,
  • dialogue actions (cancel, next step, save, etc),
  • displaying information depending on the backend response,
  • pipes/directives receiving different types of the parameter(s),
  • and many more.

Let’s cover one of the above. Imagine that in your template you need to shorten a big number value. A “K” letter should be added whenever the value is greater than 9999. Additionally, the value should be rounded and truncated.

Here are some examples of provided and expected (=>) values:

  • 0 => 0
  • 123 => 123
  • 9999 => 9999
  • 10000 => 10K
  • 12656.123 => 13K
  • if NaN then just return value pristine

Let’s do it in Angular way and create a pipe. The implementation would be like this:

It is just one of the multiple solutions of such transformation so let’s not focus on that. More important here is how to test it properly, having edge cases covered as well. Using DDT specs could be written as follows:

As you can see we’ve managed to cover cases like negative and positive values as well as correct and incorrect ones. We’ve also tested some edge cases which, in our opinion, is very crucial to have such functionality working as expected. Let’s imagine that a new requirement arrives, like replacing 1,000,000 into 1M. Then, from a testing point of view, it is just a matter of adding a few lines of additional cases like:

These arrays we create can be more complex and contain advanced fields like actions.

Let’s assume that you want to test the same behavior triggered by different user operations. It could be for example: when the user clicks on the “Save”, “Cancel” or “X” button or hits the Escape key, then close the dialogue. The testing array would be created as follows:

Summary

As you can see only by extracting some repetitions as reusable functions we can make test files more readable. Their names are also important, you’re able to immediately understand what it does.
By introducing DDT in our specs we gain a lot: more test cases and, at the same time, less code. Also having all things condensed into one array of objects gives you the ability to manage it in a more efficient way and have full control over it.

Of course, those tips and tricks described above are just the tip of the iceberg. We have a lot of ideas on how to improve the readability and usability of tests. If you wish you can read the next part of the series. You will learn how to create and use reusable testing modules.

--

--