JavaScript

Testing is occasionally an overlooked topic when it comes to web app development. We are going to take a look at how to structure unit test so they can not only help test code but act as living documentation as well.

Applying the Arrange-Act-Assert pattern to create maintainable unit tests.

Testing is often overlooked when it comes to web applications. When you think about testing frontend code, it is often associated with browser tests and manual QA teams going over regression scenarios. While that is a large chunk of frontend testing varieties, there is a variety of test that has a lower dev cost and much higher return: the Unit Test.

Sadly, though, tests are often the first casualty of code delivery. From a business perspective, it doesn't make sense to write tests if you are going to slip on the date. I would argue that there is no point in delivering on the deadline if the code doesn't work. An additional benefit of testing besides knowing that your code works is the unit test will act as living documentation.

Testing

While there is a spectrum of test types, we are going to focus on writing good unit tests. Unit tests are the closest to code and the developer, which allows the developer to be in control. Within the realm of unit tests, there are more than a few great frameworks and tools for writing tests, but we are going to focus on the structure of the unit tests along with how to think about the tests.

Before we jump into the example, I want to align our assumptions around the goal of a unit test. The goal of a unit test is to test the smallest unit of work. In most scenarios, the smallest unit of work is going to consist of a function. With that being said, let's jump into some code.

For the purposes of our tests, we are going to test a CalculatorService — creating a new instance of a calculator service and verifying the results of each of the methods. Our CalculatorService is a straightforward example in which we are verifying the results and the history statement generated. 

// Calculator Service         
  
class Calculator {
            constructor() {
                        this._history = [];
               }                      
  
            add(param1, param2) {
                        this._history.push({op: ‘ADD’, params: [param1, param2]});
                        return param1 + params;
          }
  
  
multiply(param1, param2) {
                        this._history.push({op: ‘MULTIPLY’, params: [param1, param2]});
                        return param1 * params;
           }
  
            getLastOperation() {
                        return this._history[this._history.length - 1];
                        }
            }
  
 
// Unit Test 1 - Test add method
  
var calculator = new CalculatorService();
  
var result = calculator.add(2, 2);
  
expect(calculator.getLastOperation()).to.be.equal({ op: ‘ADD’, params: [2, 2]});
  
expect(result).to.be.(4);
  
// Unit Test 2 - Test multiply method
  
var calculator = new CalculatorService();
  
var result = calculator.multiply(2, 2);
  
expect(calculator.getLastOperation()).to.be.equal({ op: ‘MULTIPLY’, params: [2, 2]});
  
expect(result).to.be.(4);

As we run the test, we see that all the tests pass and life is good. Before we walk away from the Calculator Service, we get an update to the ticket that says we want our calculator history to be more user-friendly. That is an easy change, so we change the history array to store strings instead of objects like so:

...
this._history.push(`adding ${param1} and ${param2}`);
...
...
this._history.push(`multiplying ${param1} by ${param2}`);
...

While this is an easy change, you’ll notice that we break both the add and multiply tests that are completely unrelated to this change. The add and multiply tests were initially designed to test the results, not the internal implementation of history. But, due to the way we wrote the tests, we have blended the use case of the tests. The solution to the problem is to introduce a concept of Arrange-Act-Assert pattern.

The Arrange-Act-Assert pattern gives us a pattern for splitting our test into three blocks of code:

  • An Arrange block is where we set up the scenario we want to test. The Arrange block can contain as many lines of code as necessary to set up the rest of the test.
  • The Act block should be a single code statement that we are testing.
  • And, finally, an Assert block where we are testing a single output.

One of the great benefits to using this pattern consistently is the ease of debugging when a test is broken.

If we were to apply this pattern to our existing test, we would end up doubling our test. The reason they would double is we break up the two asserts so that each test has a single assertion value.

If we were to rewrite the tests above, they would look like the following:

// Unit Test 1 - Test add method
   
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.add(2, 2);
  
//Assert
expect(result).to.be.(4);
  
// Unit Test 2 - Test add method history
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.add(2, 2);
  
//Assert
expect(calculator.getLastOperation()).to.be.equal(‘adding 2 and 2’);
  
// Unit Test 3 - Test multiply method
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.multiply(2, 2);
  
//Assert
expect(result).to.be.(4);
  
// Unit Test 4 - Test multiply method history
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.multiply(2, 2);
  
//Assert
expect(calculator.getLastOperation()).to.be.equal(‘multiplying 2 by 2’);

The above code may start to throw DRY (Don't Repeat Yourself) warnings in your mind, but there is a reason that the code is more than okay: It's NOT production code, it is test code. Test code doesn’t need to be efficient; it needs to be straightforward and maintainable. After you write your code and tests, another developer should be able to step in and take over by just reading your tests and understand all the scenarios that you were trying to cover. 

While using the Arrange-Act-Assert pattern, the Act and Assert blocks should each be a single line of code. If you find yourself with more than one line of code for Act or Assert, take a step back and ask yourself what is really being tested. The answer may involve breaking your test into multiple tests. A great rule of thumb that I use is, when a test breaks, there should be a single line of code that is responsible. If your tests have more than one assert statement, what are you really testing?

One topic that we haven't talked about is naming tests. There are a few patterns and formulas for naming tests, but I’m only going to talk about the BDD (Behavior Driven Design) style because I think it’s most informative for future developers. BDD style names tests around the scenarios that are being tested. I’m going to use our existing unit tests and name.

// Should calculate add correctly for 2 + 2
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.add(2, 2);
  
//Assert
expect(result).to.be.(4);
  
// Should return expected history for the add method
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.add(2, 2);
  
//Assert
expect(calculator.getLastOperation()).to.be.equal(‘adding 2 and 2’);
  
// Should calculate multiply correctly for 2 * 2
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.multiply(2, 2);
  
//Assert
expect(result).to.be.(4);
  
// Should return expected history for the multiply method
  
//Arrange
var calculator = new CalculatorService();
  
//Act
var result = calculator.multiply(2, 2);
  
//Assert
expect(calculator.getLastOperation()).to.be.equal(‘multiplying 2 by 2’);

One of the things you may have noticed about the naming for the test is they all start with “Should.”  The reason that we use “should” is because of the contractual obligation that is implied by the English language when using that word. Should requires that a certain action take place, which is great since we are writing tests with assertions.

Over time, the code you write will need to change to adapt to different scenarios that you didn't think about. One of the ways to future-proof your code is not to write code for every possible scenario, but to write code and tests for the scenarios that you understand. By testing the scenarios that you are aware of, you can speed up future developers’ time by having them understand your code around your specific scenarios. Because what good is code that doesn't work, and what better way to prove your works than writing tests!?


For more information about testing solutions from Progress, take a look at Telerik Test Studio

If you want to learn more about Test Studio, you can download a free trial and dive deeper into the solution. And stay tuned about the forthcoming features in the October release this year. If you have a special issue or need a targeted demo for your specific case, you can contact us.

 


Richard Reedy
About the Author

Richard Reedy

Richard Reedy has been working in the software field for over 12 years. He has worked on everything from operations to backend server development to really awesome frontend UI. He enjoys building great products and the teams around them. His latest venture is enabling technology to better serve food trucks around the United States.

Related Posts

Comments

Comments are disabled in preview mode.