Some Words Regarding Client-side Testing And Code Quality


Testing and code quality is, perhaps, not the most exciting topic to address when writing about JavaScript and client-side programming, but, it’s important. Whether you’re working in a large team, or by yourself, if you don’t test and write maintainable code, you will ship bugs.

All good programmers strive to write high-quality and bug-free code. High-quality code is code that is easy to read and understand; judiciously optimized; simple to maintain; well-documented; covered by unit tests and free of defects. But, keep in mind, you need to balance striving for maintainable and bug-free code with the dictate that the only code that matters is code that ships.

Write clear code

Clever or succinct code isn’t always the best choice. Generally, it’s more maintainable to err on the side of a few extra lines if it makes the code more readable, understandable and explicit.

Here’s a trivial JavaScript example that illustrates the point:

// too clever
var result = {
    foo: 'bar',
    baz: 'qux'
}['foo'];

// clearer; more explicit
var obj = {
    foo: 'bar',
    baz: 'qux'
};
var result = obj.foo;

Of course, clarity is a moving target (and the cause of much debate). Code that seems overly clever today is a well-understood convention tomorrow. What seems clear to me, may look like gibberish to you. If you’re working on a team, consensus should dictate code clarity (and should be automatically enforced by using a linting tool). If you’re working alone try to imagine how your future self – one that isn’t in the thick of the code now – might want the codebase to look in 6 to 12 months.

Clarity isn’t simply how you organize your code, but what you choose as your building blocks. When standard objects and APIs offer good solutions, they should be preferred. For example, when deciding how to bind a context to a function, Function.prototype.bind should be preferred over jQuery.proxy; native Promise should be preferred over jQuery.Deferred. This can get problematic as some standard APIs don’t offer good solutions by being overly verbose or non-performant. But, standards are generally better-documented and understood, have more community support and should be relatively future-proof. And keep in mind that, depending on what browsers you support, you may need to use a polyfill.

Inevitably, you’ll need to use 3rd-party, non-standard libraries. When you need to choose a non-standard library, keep these two points in mind:

  • Select libraries that are already part of your codebase. This sounds like a no-brainer. It’s so obvious! But, it’s common for new (and even experienced) team members on large projects or monolithic codebases to reinvent the wheel instead of reviewing the codebase looking for prior solutions.
  • In the event you need a new library, choose an open-source library that is well-documented, well-tested and has a community on Github. In other words, look for a de-facto community standard (e.g. lodash). If you can’t find one for your use-case, it may be better to write, test and document one yourself.

There’s lots more to be said on this subject. Nicholas Zakas’s Maintainable JavaScript is a good resource. Also, the ESlint project allows you to automatically enforce code-quality and style issues in a JavaScript codebase. For reference, here’s 1stdibs’s ESlint configurations.

Document your code

One great way to document your code is by using JSDoc comments. If you’re using an IntelliJ-derived IDE (e.g. WebStorm, PHPStorm), there’s a handy plugin that will generate a template for you (see JetBrain’s documentation). For Sublime Text users, check out this plugin.

Even if you aren’t generating documentation from the JSDoc comments, the format is well-understood and creates a good base for documentation in your project. Additionally, many IDEs love it and offer inline documentation, type-checking and auto-complete when JSDoc comments are present.

Self-documenting code also has its uses. Variable, function and method names should be written in a descriptive and semantically-sensible way within the proper scope and context of the code. Care should be given not to make names too long and unwieldy. Good JavaScript shouldn’t be written in a way that contains large scopes. Locally-scoped variables and functions can have shorter names that are sensible and self-documenting within their context.

A simple example is below. It’s obvious what the following function does by reading its name.

var id = getId(model);

The example function definition:

/**
 * @param {object} model A model with an optional id property
 * @returns {number|undefined}
 */
var getId = function (model) {
    return model.id;
};

In this example, the @description field can be omitted since the function name is self-documenting. It would be redundant to write “@description Retrieves the id of model.” That doesn’t mean one shouldn’t write JSDocs for self-documenting functions. The arguments, return values and other non-obvious information need to be documented.

When writing a library package that is meant to be shared across teams (or open-sourced), a readme that documents the API and offers examples of its usage should always be included.

Deodorize your code

A code smell is a surface indication that usually corresponds to a deeper problem in the system

Eliminating code smell is a huge topic so this article won’t go into it in any depth. Instead, check out this great blog post and video by Elijah Manor. He covers this area very thoroughly.

As a TL;DR, here two common issues to be on the lookout for:

  1. Too much complexity – if a program’s or function’s cyclomatic complexity gets too great it means code is hard to follow and debug. ESlint provides an automated way to test for too much complexity.
  2. copy and paste problems – following already erroneous patterns or reusing a pattern you don’t fully understand in a faulty way (also called cargo-culting)

But, seriously, watch Elijah Manor’s video.

Test your code

The importance of testing your code cannot be stressed enough. As a developer, you should be confident that your code works in all imagined scenarios when it leaves your hands. Whether this entails the code being sent to a continuous integration server, going into a code review, released to a QA team or shipping to production, the developer should feel 100% confident in her code.

Programmers should be focused on automated tests.

Unit tests

You should spend the bulk of your testing time writing automated tests. As a front-end or client-side developer this means writing unit tests.

One isn’t writing quality code if one isn’t writing unit tests.

Time spent manually testing code is lost time. Every time a modification is made, the code needs to be manually tested again. And again. And again. However, unit tests, once written, can be run over and over without wasting more valuable time. They allow you to drastically change implementations of units of code and be confident that your changes aren’t breaking downstream consumers.

As a developer, you should be confident that your code works in all imagined scenarios when it leaves your hands.

In addition to exercising code, unit tests are one of the best ways to document the programmer’s intentions since there are concrete assertions in the code as to how the code is meant to function.

At 1stdibs, on the front-end team, unit tests run on each Github pull request and during each Jenkins build. Also, developers are strongly encouraged to run tests locally before pushing code to their forks. The 1stdibs front-end team is using npm as our JavaScript package manager and have integrated it deeply into our workflow. Running tests can be made very simple and convenient by adding npm test scripts to repositories.

Functional tests

If you have the luxury of running functional tests against a fully-integrated web site, then you could do worse than using webdriver.io and selenium. In complex projects and large teams, it’s good to have a developer or team that owns these tests and runs them after QA, staging and production builds.

Manual tests

Ugh. Manual testing. Everyone hates testing manually. It is literally a waste of time. Unfortunately, for fine-grained interactions, CSS layout specifications, complex UI or just simple smoke tests manual testing is sometimes the only (or best) option.

You should – at a minimum – test the happy path and page layouts in the desktop browsers and devices that you support. What browsers and devices you choose to support is a tricky question. Whatever you decide, the decision should be informed by analytics based on your target audience or real traffic.

CSS and JavaScript inconsistencies have always made it required that a good client-side engineer manually test across browsers and devices (although, this has gotten better in recent years). The introduction of ES6 (AKA ES2015) to codebases via the excellent Babel library has made cross-browser testing especially important (and hard). There is a mix of ES6 support across browser vendors. There is a chance of transpiling bugs cropping up when ES6 code hasn’t been processed properly. Things work fine in one browser – which happens to support some ES6 feature – only to fail spectacularly in another that doesn’t.

Cross-browser manual testing isn’t simple. On Windows or OS X, you can download and easily install the latest versions of Chrome, Firefox and Safari. Also, Microsoft has made it relatively simple to test against different versions of Internet Explorer by downloading virtual machines from modern.ie. But, to test Mobile Safari, you’ll need a device or, at the very least, use the iOS simulator that comes with Xcode (which requires OS X).

Conclusion

Whether you’re part of a large organization, freelancing or a sole proprietor it’s important to write maintainable, bug-free code.

Most software development projects need reliable time estimates from developers so that projects stay on schedule, informed ROI decisions can be made and staffing planned. Needless to say, when you’re engineering with quality in mind you need to factor in the time it takes for tests, documentation and writing maintainable code. If you’re shipping code without tests and some form of documentation, then you’re adding tech debt to your team (whether you’re on a team of 1 or 100).

There is more to client-side software development than just writing working code that executes in the user’s browser. You need to consider your organization, your team and your future self.