Shuhei Kagawa

Speed up your RSpec tests by reviewing Factory Girl

Sep 26, 2015 - Ruby, Rails

Factory Girl is a great tool that makes test data creation easier. However, if you don't use it properly, it may imperceptibly slow down your tests.

In this post, I will walk through some caveats that I stumbled upon in my current project. They could be applied not only to RSpec but I will use RSpec as an example.

Measure! Measure! Measure!

What gets measured gets managed.

Peter Drucker

First of all, we want to know which tests take most of the times. RSpec has --profile/-p option for the very purpose.

$ rspec --help
# ...
    -p, --[no-]profile [COUNT]       Enable profiling of examples and list the slowest examples (default: 10).
# ...

Let's measure your tests with it.

# Run all specs with profiling.
bin/rspec -p

# Run specific spec file with profiling.
bin/rspec spec/models/article_spec.rb -p

It shows the slowest examples and groups. They should be the good point to start with.

Database writes are slow

There are many potential causes that slow down your tests. External API calls, file access, database access and etc. Among them, I would like to focus on database writes in this post because they are ubiquitous and relatively slow.

You could use mocks/stubs to completely avoid touching database. However, you may have a certain amount of code that touches database. I believe it's better to find database-related bugs with a bit slow tests than finding them on production that were overlooked by lightning-fast tests.

One of the Rails' greatest features is that we can easily write tests that involves database queries. It's too good not to use it at all.

Before you consider parallel execution of tests that may introduce other complexity, you still have something to do.

Sample project

Let's say we have a blog application with the following models:

class Author < ActiveRecord::Base
  has_many :articles
end

class Article < ActiveRecord::Base
  belongs_to :author
  has_many :comments

  validates :author, presence: true
end

class Comment < ActiveRecord::Base
  belongs_to :article

  validates :article, presence: true

  def edited?
    created_at < updated_at
  end
end

and factories:

FactoryGirl.define do
  factory :author do
    first_name 'Shuhei'
    last_name 'Kagawa'
  end

  factory :article do
    author
    title 'Rails on Rails'
    body 'If you created an application that manages railway rails with the Rails framework, its name would be Rails on Rails...'
  end

  factory :comment do
    article
    commenter 'Railer'
    body 'Great post!'
  end
end

FactoryGirl.build creates associations

Let's review Factory Girl's create and build. create instantiates an model saving it into the database just like ActiveRecord::Base.create does. build only instantiates an model without saving it just like ActiveRecord::Base.new does.

The following usage of FactoryGirl.build seems harmless. build doesn't save a Comment into the database while create does, right?

describe Comment
  describe '#edited?' do
    it 'returns true if updated after creation' do
      now = Time.zone.now
      comment = FactoryGirl.build(:comment, created_at: now - 1.minute, updated_at: now)

      expect(comment).to be_edited
    end

    it 'returns false right after creation' do
      now = Time.zone.now
      comment = FactoryGirl.build(:comment, created_at: now, updated_at: now)

      expect(comment).not_to be_edited
    end
  end
end

However, build actually saves the model's associations, article and author created by article in this case, into the database unless you give strategy: :build option in the factory. So build actually creates all its ancestor models, which can be a huge performance penalty if called plenty of times.

To avoid this behavior, you can use FactoryGirl.build_stubbed instead of build. It builds all associations and don't save them into the database.

comment = FactoryGirl.build_stubbed(:comment, created_at: now - 1.minute, updated_at: now)

In this case, you even don't need to use Factory Girl because the edited? method doesn't involve associations. The following just works fine:

comment = Comment.new(created_at: now - 1.minute, updated_at: now)

Here is another case where unnecessary post is created by build(:comment).

post = FactoryGirl.create(:post)
post.comments << FactoryGirl.build(:comment)

You could do:

post = FactoryGirl.create(:post)
post.comments << FactoryGirl.build(:comment, post: nil)

# or

post = FactoryGirl.create(:post)
FactoryGirl.create(:comment, post: post)

Review your association chain

There is also a case where you intentionally use FactoryGirl.create and create unused objects. Let's think about Blog model that has multiple authors and multiple posts.

FactoryGirl.define do
  factory :blog do
  end

  factory :author do
    blog
  end

  factory :post do
    author
    blog
  end
end

With the setup above, FactoryGirl.create(:post) creates blog twice, once in the post factory and once in the author factory. Not only is it redundant, but also it may introduce data inconsistency because the two blog instances are different.

The post factory could reuse the author's blog.

factory :post do
  author
  blog { author.blog }
end

Summary

To recap, there are things to consider before you stub everything or start considering parallel execution of tests. Imagine what Factory Girl exactly does and review your tests and factories. You will be able to speed up your tests for relatively cheaper cost.

References

Using highlight.js with marked

Sep 21, 2015 - JavaScript

I use marked to parse markdown files of posts and pages, and highlight.js to highlight code blocks in them for this site. Here are problems that I came across to make them work together and a workaround for them.

marked's README has an example on how to configure it to work with highlight.js but it doesn't add hljs classes on <code> tags that highlight.js uses to style code blocks. At the moment, you have to prepare your own renderer to achieve it.

Also, you need to check whether the language given by marked is a valid one for highlight.js. highlight.js seems to have had LANGUAGES property a few years ago but now getLanguage() method serves as a substitute for it.

Here's the outcome:

import marked, { Renderer } from "marked";
import highlightjs from "highlight.js";

const escapeMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;"
};

function escapeForHTML(input) {
  return input.replace(/([&<>'"])/g, char => escapeMap[char]);
}

// Create your custom renderer.
const renderer = new Renderer();
renderer.code = (code, language) => {
  // Check whether the given language is valid for highlight.js.
  const validLang = !!(language && highlightjs.getLanguage(language));

  // Highlight only if the language is valid.
  // highlight.js escapes HTML in the code, but we need to escape by ourselves
  // when we don't use it.
  const highlighted = validLang
    ? highlightjs.highlight(language, code).value
    : escapeForHTML(code);

  // Render the highlighted code with `hljs` class.
  return `<pre><code class="hljs ${language}">${highlighted}</code></pre>`;
};

// Set the renderer to marked.
marked.setOptions({ renderer });

EDIT on Feb 24, 2019: The code snippet above was originally meant to be used for this blog and didn't have proper XSS protection for handling user inputs. Oleksii pointed out in a comment that it was vulnerable to XSS when language was not specified. I fixed the issue by escaping code for HTML. Thanks a lot, Oleksii!

Let's create a Babel plugin

Sep 13, 2015 - JavaScript, Babel

[EDIT] This article was written for Babal 5.x, which is outdated now. I recommend thejameskyle/babel-handbook as more up-to-date documentation.

Babel is the great tool that transpiles ES2015, ES7, JSX and such into ES5 and make them available on the browsers. If you are a person like me, you might use it on a daily basis.

In addition to to the built-in transformers, you can add your own transpilation rules by employing third-party plugins. For example, I have been developing a plugin that enables you to write Angular 2 apps with Babel lately. It is easier to develop than you may think. Let me introduce how to create a plugin for Babel 5.x.

How Babel works

Simply put, Babel works like the following:

  1. Babylon, the parser of Babel, parses source code into AST.
  2. Transformers transforms AST into another AST in sequence.
  3. Generators generates JavaScript code from the final AST.

In the step 1, AST is for Abstract Syntax Tree, which represents the structure of source code as a tree. Babel's AST is based on a specfication called ESTree and has some extensions for non-standard nodes like ES7+, JSX and flowtype. You can check the ESTree spec at the following links:

In the step 2, transformers consists of the built-in transformers, like es6.classes, and third-party plugin transformers. As of Babel 6.0, the built-in transpilers will also be extracted as external plugin modules. So there will be no border between the build-in and third-party plugins.

So, what you have to do is write a transformer that transforms AST into another AST. You don't need to parse JS or generate JS from AST by yourself. It will be greatly effective compared to introducing another tool that parses JS by its own. Also, Babel's powerful API will make it easier than using the raw esprima tools.

What you can (not) do

You can do almost anything as long as it's in the syntax that Babylon supports, ES2015, ES7+, JSX, flowtype and etc. You can't introduce new syntax because Babel currently doesn't support parser extension by plugins. (You can actually accomplish it by monkey-patching Babylon though.)

Create a project

Create a directory in the format of babel-plugin-*. The * part turns to be your plugin name. In the directory, you can create a Babel plugin project with babel-plugin init, which is installed by npm install -g babel.

# Prepare a project directory.
mkdir babel-plugin-foo-bar
cd babel-plugin-foo-bar
# Generate necessary files.
npm install -g babel
babel-plugin init
# Install dependencies.
npm install

The following structure should have been generated:

.
├── .gitignore
├── .npmigonore
├── LICENSE
├── README.md
├── node_modules
├── package.json
└── src
    └── index.js

You will find some npm-run-scripts in the package.json:

  • npm run build transpiles files under src directory with Babel and output the result into lib directory.
  • npm run push releases a newer version of the plugin. It takes care of git commit, tag and npm package.
  • npm test runs babel-plugin test but fails because the command doesn't exist. It seems like it will work in the future. So, prepare your favorite test runner like mocha for now.

With this setup, you can write your plugin itself with Babel's features. src should be ignored in npm and lib in git.

Transformer

src/index.js, the meat of the plugin, looks like this:

/* eslint no-unused-vars:0 */
export default function({ Plugin, types: t }) {
  return new Plugin("foo", {
    visitor: {
      // your visitor methods go here
    }
  });
}

It exports a factory function that creates a Plugin instance. The Plugin constructor gets the plugin's name and a configuration object.

The visitor property holds methods named as AST node types. A Babel transformer traverses AST from the top to the bottom. Each method is called when the trasnformer visits the matched nodes. For instance, you can manipulate class declarations and function declarations as the following:

export default function({ Plugin, types: t }) {
  return new Plugin("foo", {
    visitor: {
      ClassDeclaration(node, parent) {
        // Do something on a class declaration node.
      },
      FunctionDeclaration(node, parent) {
        // Do something on a function declaration node.
      }
    }
  });
}

You can also use alias instead of plain node types to match multiple node types. For example, Function matches against FunctionDeclaration and FunctionExpression.

types is another important thing. It contains a bunch of utility functions for AST manipulation.

  • AST node generation functions such as identifier(), memberExpression() and assignmentExpression. Their names are lowerCamelCased versions of the corresponding node types. You can check their arguments at definitions' builder properties.
  • AST node check functions such as isIdentifier() and isDecorator(). You can shallowly check node properties with the second argument.

The functions are generated from definitions。The definitions will serve as a reference.

The best examples of AST transformation using types functions are undoubtedly the source code of the built-in transformers. Pick the closest one to what you want from the list and check out the source code. The official documentation will also help.

Case study: Assign class constructor's arguments as instance properties

As a case study, I built a plugin called babel-plugin-auto-assign that "assigns class constructor's arguments as instance properties", which resembles TypeScript's parameter properties in its action. It is intended to be used with class-based dependency injection like Angular's.

To avoid unawarely messing up constructors, let's apply the transformation only to classes with a decrator called @autoAssign. @autoAssign is a so-called ambient decorator because it should not appear in the output.

Before:

@autoAssign
class Hello {
  constructor(foo, bar, baz) {}
}

After:

class Hello {
  constructor(foo, bar, baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

Note that we can leave the ES6 class as is because it's going to be transformed to ES5 by the subsequent built-in transformers. Babel plugin transformers are applied before the built-in transformers by default. If you want to apply a plugin after the built-in transformers, suffix the plugin name with :after like babel --plugins foo:after index.js.

AST before/after transformation

To transform AST, we need to know how the starting post and the goal look like. You can visualize source code in AST parsed by Babylon with Felix Kling's JS AST Explorer.

It also works to examine nodes with console.log().

Code

Once you get the ASTs, half of the work is done. Let's write some code to insert AST nodes using types functions.

The complete project includes unit testing with fixtures.

src/index.js

import AutoAssign from "./auto-assign";

export default function({ Plugin, types: t }) {
  return new Plugin("autoAssign", {
    visitor: {
      ClassDeclaration: function(node, parent) {
        new AutoAssign(t).run(node);
      }
    }
  });
}

src/auto-assign.js

export default class AutoAssign {
  constructor(types) {
    this.types = types;
  }

  run(klass) {
    // Process only if `@autoAssign` decorator exists.
    const decorators = this.findautoAssignDecorators(klass);
    if (decorators.length > 0) {
      // Get constructor and its paremeters.
      const ctor = this.findConstructor(klass);
      const args = this.getArguments(ctor);
      // Prepend assignment statements to the constructor.
      this.prependAssignments(ctor, args);
      // Delete `@autoAssign`.
      this.deleteDecorators(klass, decorators);
    }
  }

  findautoAssignDecorators(klass) {
    return (klass.decorators || []).filter(decorator => {
      return decorator.expression.name === "autoAssign";
    });
  }

  deleteDecorators(klass, decorators) {
    decorators.forEach(decorator => {
      const index = klass.decorators.indexOf(decorator);
      if (index >= 0) {
        klass.decorators.splice(index, 1);
      }
    });
  }

  findConstructor(klass) {
    return klass.body.body.filter(body => {
      return body.kind === "constructor";
    })[0];
  }

  getArguments(ctor) {
    return ctor.value.params;
  }

  prependAssignments(ctor, args) {
    const body = ctor.value.body.body;
    args
      .slice()
      .reverse()
      .forEach(arg => {
        const assignment = this.buildAssignment(arg);
        body.unshift(assignment);
      });
  }

  buildAssignment(arg) {
    const self = this.types.identifier("this");
    const prop = this.types.memberExpression(self, arg);
    const assignment = this.types.assignmentExpression("=", prop, arg);
    return this.types.expressionStatement(assignment);
  }
}

Run!

Use --optional es7.decorators option in order to support decorators. You can specify plugins by file path in addition to plugin name, which is convenient for development.

npm run build
echo '@autoAssign class Hello { constructor(foo, bar, baz) {} }' | babel --optional es7.decorators --plugins ./lib/index.js

Here comes the result!

"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Hello = (function() {
  function Hello(foo, bar, baz) {
    _classCallCheck(this, _Hello);

    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }

  var _Hello = Hello;
  return Hello;
})();

Publish

After you write README and commit it, you can publish your plugin to the world by npm run push.

Let's create awesome babel plugins!

References

Documentation

Projects