A Favor for Your Future Self

We tend to think about the future when we build things. What might we want to be able to add later? How can we refactor this down the road? Will this be easy to maintain in six months, a year, two years? As best we can, we try to think about the what-ifs, and build our websites, systems, and applications with this lens.

We comment our code to explain what we knew at the time and how that impacted how we built something. We add to-dos to the things we want to change. These are all great things! Whether or not we come back to those to-dos, refactor that one thing, or add new features, we put in a bit of effort up front just in case to give us a bit of safety later.

I want to talk about a situation that Past Alicia and Team couldn’t even foresee or plan for. Recently, the startup I was a part of had to remove large sections of our website. Not just content, but entire pages and functionality. It wasn’t a very pleasant experience, not only for the reason why we had to remove so much of what we had built, but also because it’s the ultimate “I really hope this doesn’t break something else” situation. It was a stressful and tedious effort of triple checking that the things we were removing weren’t dependencies elsewhere. To be honest, we wouldn’t have been able to do this with any amount of success or confidence without our test suite.

Writing tests for code is one of those things that developers really, really don’t want to do. It’s one of the easiest things to cut in the development process, and there’s often a struggle to have developers start writing tests in the first place. One of the best lessons the web has taught us is that we can’t, in good faith, trust the happy path. We must make sure ourselves, and our users, aren’t in a tough spot later on because we only thought of the best case scenarios.

JavaScript

Regardless of your opinion on whether or not everything needs to be built primarily with JavaScript, if you’re choosing to build a JavaScript heavy app, you absolutely should be writing some combination of unit and integration tests.

Unit tests are for testing extremely isolated and small pieces of code, which we refer to as the units themselves. Great for reused functions and small, scoped areas, this is the closest you examine your code with the testing microscope. For example, if we were to build a calculator, the most minute piece we could test could be the basic operations.

/*
 * This example uses a test framework called Jasmine
 */

describe("Calculator Operations", function () {

    it("Should add two numbers", function () {

    // Say we have a calculator
        Calculator.init();

    // We can run the function that does our addition calculation...
        var result = Calculator.addNumbers(7,3);

    // ...and ensure we're getting the right output
        expect(result).toBe(10);

    });
});

Even though these teeny bits work in isolation, we should ensure that connecting the large pieces work, as well. This is where integration tests excel. These tests ensure that two or more different areas of code, that may not directly know about each other, still behave in expected ways. Let’s build upon our calculator - we may want the operations to be saved in memory after a calculation runs. This isn’t as suited for a unit test because there are a few other moving pieces involved in the process (the calculations, checking if the result was an error, etc.).

    it(“Should remember the last calculation”, function () {

    // Run an operation
        Calculator.addNumbers(7,10);

    // Expect something else to have happened as a result
        expect(Calculator.updateCurrentValue).toHaveBeenCalled();
        expect(Calculator.currentValue).toBe(17);
    });

Unit and integration tests provide assurance that your hand-rolled JavaScript should, for the most part, never fail in a grand fashion. Although it still might happen, you could be able to catch problems way sooner than without a test suite, and hopefully never push those failures to your production environment.

Interfaces

Regardless of how you’re building something, it most definitely has some kind of interface. Whether you’re using a very barebones structure, or you’re leveraging a whole design system, these things can be tested as well.

Acceptance testing helps us ensure that users can get from point A to point B within our web things, which can provide assurance that major features are always functioning properly. By simulating user input and data entry, we can go through whole user workflows to test for both success and failure scenarios. These are not necessarily for simulating edge-case scenarios, but rather ensuring that our core offerings are stable.

For example, if your site requires signup, you want to make sure the workflow is behaving as expected - allowing valid information to go through signup, while invalid information does not let you progress.

/*
 * This example uses Jasmine along with an add-on called jasmine-integration
 */

describe("Acceptance tests", function () {

  // Go to our signup page
  var page = visit("/signup");

  // Fill our signup form with invalid information
  page.fill_in("input[name='email']", "Not An Email");
  page.fill_in("input[name='name']", "Alicia");
  page.click("button[type=submit]");

  // Check that we get an expected error message
  it("Shouldn't allow signup with invalid information", function () {
    expect(page.find("#signupError").hasClass("hidden")).toBeFalsy();
  });

  // Now, fill our signup form with valid information
  page.fill_in("input[name='email']", "thisismyemail@gmail.com");
  page.fill_in("input[name='name']", "Gerry");
  page.click("button[type=submit]");

  // Check that we get an expected success message and the error message is hidden
  it("Should allow signup with valid information", function () {
    expect(page.find("#signupError").hasClass("hidden")).toBeTruthy();
    expect(page.find("#thankYouMessage").hasClass("hidden")).toBeFalsy();
  });
});

In terms of visual design, we’re now able to take snapshots of what our interfaces look like before and after any code changes to see what has changed. We call this visual regression testing. Rather than being a pass or fail test like our other examples thus far, this is more of an awareness test, intended to inform developers of all the visual differences that have occurred, intentional or not. Developers may accidentally introduce a styling change or fix that has unintended side effects on other areas of a website - visual regression testing helps us catch these sooner rather than later. These do require a bit more consistent grooming than other tests, but can be valuable in major CSS refactors or if your CSS is generally a bit like Jenga.

Tools like PhantomCSS will take screenshots of your pages, and do a visual comparison to check what has changed between two sets of images. The code would look something like this:

/*
 * This example uses PhantomCSS
 */

casper.start("/home").then(function(){

  // Initial state of form
  phantomcss.screenshot("#signUpForm", "sign up form");

  // Hit the sign up button (should trigger error)
  casper.click("button#signUp");

  // Take a screenshot of the UI component
  phantomcss.screenshot("#signUpForm", "sign up form error");

  // Fill in form by name attributes & submit
  casper.fill("#signUpForm", {
    name: "Alicia Sedlock",
    email: "alicia@example.com"
  }, true);

  // Take a second screenshot of success state
  phantomcss.screenshot("#signUpForm", "sign up form success");
});

You run this code before starting any development, to create your baseline set of screen captures. After you’ve completed a batch of work, you run PhantomCSS again. This will create a second batch of screenshots, which are then put through an image comparison tool to display any differences that occurred. Say you changed your margins on our form elements – your image diff would look something like this:

An image diff of a form element that has had its margins changed. The diff is highlighted in pink.

This is a great tool for ensuring not just your site retains its expected styling, but it’s also great for ensuring nothing accidentally changes in the living style guide or modular components you may have developed. It’s hard to keep eagle eyes on every visual aspect of your site or app, so visual regression testing helps to keep these things monitored.

Conclusion

The shape and size of what you’re testing for your site or app will vary. You may not need lots of unit or integration tests if you don’t write a lot of JavaScript. You may not need visual regression testing for a one page site. It’s important to assess your codebase to see which tests would provide the most benefit for you and your team.

Writing tests isn’t a joy for most developers, myself included. But I end up thanking Past Alicia a lot when there are tests, because otherwise I would have introduced a lot of issues into codebases. Shipping code that’s broken breaks trust with our users, and it’s our responsibility as developers to make sure that trust isn’t broken. Testing shouldn’t be considered a “nice to have” - it should be an integral piece of our workflow and our day-to-day job.

About the author

Alicia Sedlock is a front-end web developer who encourages and teaches developer to build responsibly. She thought she’d be able to make a living building MySpace and Livejournal themes, but has since progressed to helping teams building out extensible front-end systems, and is currently a UI Architect at ThirdChannel. When not building things, Alicia is an avid gamer, and tweets a lot of pictures of her hedgehog, Mabel.

More articles by Alicia

Comments