PPL 2020 — Document Builder: How We Test Our App With Jest
As already told in previous articles, at PPL 2020, we are told to maintain a good code coverage using testing, to ensure that our app, Document Builder, is working according to its requirements. To accomplish this, we need a good testing framework to make it easier for us to test our app. Fortunately, both our backend and frontend are based on JavaScript. React, the library we use to build our frontend already comes with a nice testing framework called Jest, which can be used to test any JavaScript-based project.
How We Test Our Backend
When we want to test our backend, we want to ensure that the behaviour of our function matches all the scenarios. This includes testing that it calls the database correctly, do calls another function, and return the correct response given a request. We also test that the functions handle error correctly.
To do this, we do unit testing, where we a test focus on one and only one piece of code, usually a function, and check whether it could pass all of our scenarios. Isolating a function requires mocking or stubbing the dependencies, where we essentially intercept any call from the function to the dependencies and return a value according to the scenario we created (we’ll revisit this later). This is easily accomplished with a module for Jest called jest-when, which simplify mocking in Jest.
Our unit testing consists of 3 stages, called Arrange, Act, and Assert. On the Arrange stage, we define data that needs to be mocked (data that the function gets from call to external functions) and the behaviour of any call to an external function. As pictured above, we want to test if our findById function correctly calls the database and return a document with template and templateDocument metadata when given document id and options set to true. Because we only testing the findById function here, we don’t actually care what the database gives us. We only care that if we give a document id to the findById function, will it call the database, and when that database gives back the data, will the function return its value or not. So, we create a dummy data, and we arrange so that when our function does call the database with the specified parameters, it will return that dummy data.
On the Act stage, we call the function with the required parameters and we store its result for checking later. As our current scenario is testing that whether findById correctly calls the database and return a document with template and templateDocument metadata when given document id and options set to true, we call the function with a document id and options object where all the options are set to true.
Last, the Assert stage, where we check if our function is working as expected or not. Here, we check if our function really calls the database or not, and we check if the return value is within our expectation or not.
That’s it! Testing in the backend is as simple as arranging the data that needs to be mocked, calling the function, and asserting that the values and behaviours match our expectation. The majority of our backend tests are structured like this, although there is some minor quirk, such as testing HTTP Response, where the Act and Assert stage differ a little bit, and when we have to do a more thorough mocking and stubbing, which I will explain at the bottom of this article after explaining our philosophy on the frontend testing.
Our Focus when Testing Frontend
Testing frontend, to say the least, is difficult. Quoting from my former mentor on one of my internship, testing on the frontend is “an affair that causes more headache than reducing it”. None of us ever do testing on the frontend, and thus we are a bit lost on this front. We considered some options, like Snapshot Testing that compares the UI with a reference file and Selenium-like approach which simulate user behaviour by clicking and checking the rendered elements. Eventually, we agree to test our frontend using an approach that is a hybrid between our backend testing philosophy, which is program behaviour (such as API call and state changes) focused testing, and Selenium-like approach where we also check UI navigational behaviour such as pop-up on click and navigate on a click. We use this approach because:
- Testing UI like checking all elements and Snapshot Testing is cumbersome, due to that the UI changes frequently, requiring us to rewrite numerous tests despite essentially no behaviour changes. For example, we are told by our client to change one of our Pages from using buttons with icon only to buttons with icon and text. The behaviour stays the same, yet the UI elements change. If we do testing on all the elements, we have to change the tests, yet if we only test the UI behaviour, we won’t have to change the tests.
- For us, broken UI behaviour is more important than a slight difference in the UI element. Pages that won’t load its data or fails to show a pop-up on error is far scarier than a miscoloured button.
As pictured above, we still using the Arrange-Act-Assert pattern on fronted testing. The difference from testing on the backend is subtle, such as the Act stage, which follows React guideline on testing like awaiting the render because render on React is an asynchronous task. The rest is fairly similar, such as mocking API call which is similar to mocking call to an external function and asserting pop-up and navigation which is convenient because both are represented by swal package and navigate function.
Mocking or Stubbing
Above, we frequently say mocking and stubbing. What is exactly mocking and stubbing? As explained above, unit testing focus on one and only one piece of code. So, dependencies of the piece of code that is currently tested are none of our concern, and thus it’s real implementation should not be called so it doesn’t interfere with the test. Example of this interference is if the dependencies are currently broken, so if we use it’s real implementation, our test will fail, and we are misled into thinking that the piece of code that is currently tested is broken whereas, in fact, it’s the dependencies that are broken. Thus, isolating our code helps us to pinpoint where the broken code truly is.
But, if we couldn’t call the real implementations of the dependencies, then our piece of code couldn’t work correctly, isn’t it? Yes, but here is where mocking and stubbing comes into play. With mocking, you could simulate the dependencies, thus making your piece of code works. Let’s revisit our jest-when example above.
On our findById function, it calls the database through the Document.findOne function. Following our policy of isolation, we mock this function, so that when it is called, instead of calling the real database, we simply intercept that call and return a desired value that is suitable to the current test scenario. Using this, we could simulate our dependencies, from returning a value, to returning an error.
Stubbing is subtly different from Mocking. As I understand it, Mocking is typically used to simulate and verify behaviour of object or function that is mocked, while stubbing only simulate the behaviour. But the definition of Mocking and Stubbing is not universally accepted in the programming community. Search google and you will find that testing frameworks often contradict each other on what is a mock and what is a stub. On our app, we’ll say that we use only mocks due to the fact that jest call all of this mock.
Note that stubbing and mocking are only used in Unit Testing. This is because unit testing only tests a section of code, and simulate other codes that the section we testing depends on. Simulating that code is done using stubbing and mocking. On other types of test, such as Integration Testing, or Testing using Selenium, doesn’t need mocking because those tests aren’t testing a section of code, but the whole app, thus no part of codes needs to be simulated using mocking and stubbing.
Conclusion
Due to this experience, we now know that testing is not a simple affair. Testing differs from language to language, framework to framework, even backend and frontend have a different testing procedure, and thus expertise in testing on one language doesn’t translate into proficiency in testing on all language. We should weight the advantages versus the disadvantages of several testing procedures, and pick the one that will benefit our project the most to leverage the most value out of testing for our project.
Thank you for reading!