Skip to content

Instantly share code, notes, and snippets.

@pixelhandler
Last active April 11, 2017 14:30
Show Gist options
  • Save pixelhandler/3865763ba3a8a8f7529823e0419adc5b to your computer and use it in GitHub Desktop.
Save pixelhandler/3865763ba3a8a8f7529823e0419adc5b to your computer and use it in GitHub Desktop.
Outside in TDD with Ember

Outside in TDD with Ember

A “double-loop” TDD process, write a functional test first and then the unit tests.

Although my previous experience had certainly opened my mind to the possible benefits of automated testing, I still dragged my feet at every stage. “I mean, testing in general might be a good idea, but really? All these tests? Some of them seem like a total waste of time… What? Functional tests as well as unit tests? Come on, that’s overdoing it! And this TDD test/minimal-code-change/test cycle? This is just silly! We don’t need all these baby steps! Come on, we can see what the right answer is, why don’t we just skip to the end?”

Believe me, I second-guessed every rule, I suggested every shortcut, I demanded justifications for every seemingly pointless aspect of TDD, and I came out seeing the wisdom of it all. I’ve lost count of the number of times I’ve thought “Thanks, tests”, as a functional test uncovers a regression we would never have predicted, or a unit test saves me from making a really silly logic error. Psychologically, it’s made development a much less stressful process. It produces code that’s a pleasure to work with.

The alternative to “Outside In” is to work “Inside Out”, which is the way most people intuitively work before they encounter TDD. After coming up with a design, the natural inclination is sometimes to implement it starting with the innermost, lowest-level components first.

(Excerpts From: Harry J.W. Percival. “Test-Driven Development with Python.”)

Functional testing

Functional testing is a software testing process used within software development in which software is tested to ensure that it conforms with all requirements. Functional testing is a way of checking software to ensure that it has all the required functionality that's specified within its functional requirements.

Functional testing tests a slice of functionality of the whole system

Unit testing

Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation. Unit testing can be done manually but is often automated.

Acceptance tests

In ember an “Acceptance Test” is our functional test, see testing/acceptance guide.

Almost every test has a pattern of visiting a route, interacting with the page (using the helpers), and checking for expected changes in the DOM.

Helpers

Asynchronous Helpers

  • click(selector)
  • fillIn(selector, value)
  • keyEvent(selector, type, keyCode)
  • triggerEvent(selector, type, options)
  • visit(url)

Synchronous Helpers

  • currentPath()
  • currentRouteName()
  • currentURL()
  • find(selector, context)

Wait Helpers

  • andThen, e.g. andThen( () => assert.equal(/* … */) )

(Kind of like Selenium Webdriver.)

As an alternative to the ember-testing helpers above, use the native DOM helpers:

import { visit, click, find, fillIn } from 'ember-native-dom-helpers';

Test selectors

In a template:

  • <h1 data-test-post-title>{{post.title}}</h1>

In your tests:

  • assert.equal(find(testSelector('post-title')).text(), 'blog post');

Integration Tests

Use, moduleForComponent helper from ember-qunit.

Each test following the moduleForComponent call has access to the render() function, which lets us create a new instance of the component by declaring the component in template syntax, as we would in our application

this.render(hbs{{magic-title}});

Components are a great way to create powerful, interactive, and self-contained custom HTML elements.

Also use native DOM helpers for similar helpers to the ember-testing DSL found in the acceptance tests.

import { click, fillIn, find, findAll, keyEvent, triggerEvent } from 'ember-native-dom-helpers';

Unit Tests

Unit tests are generally used to test a small piece of code and ensure that it is doing what was intended. Unlike acceptance tests, they are narrow in scope and do not require the Ember application to be running.

Meaningful tests

See Sandi Metz advice for writing tests: rules for good testing gist.

  • Queries are messages that "return something" and "change nothing".
  • Commands are messages that "return nothing" and "change something".

What to test?

  • Test incoming query messages by making assertions about what they send back
  • Test incoming command messages by making assertions about direct public side effects

What NOT to test?

  • Messages that are sent from within the object itself (e.g. private methods).
  • Outgoing query messages (as they have no public side effects)
  • Outgoing command messages (use mocks and set expectations on behaviour to ensure rest of your code pass without error)
  • Incoming messages that have no dependants (just remove those tests)

Note: there is no point in testing outgoing messages because they should be tested as incoming messages on another object.

What to Mock/Stub

Command messages should be mocked, while query messages should be stubbed

Contract Tests

These types of tests can be useful to ensure (third party) APIs do (or don't) cause our code to break.

Code Examples

The goal here is to point you to a few places to learn more about testing.

There are some great Ember projects in the open, a couple examples.

Perhaps study some test suites and look for meaning full tests vs it works tests.

  • The goal is not: tests should break when code changes; but instead, to provide meaningful, relevant, maintainable tests which save time and resources.

A few examples below from the Code Corps project…

Acceptance

test('Logging in', function(assert) {
  assert.expect(2);

  let email = 'test@test.com';
  let password = 'password';

  server.create('user', { email, password, state: 'selected_skills' });

  loginPage.visit();

  andThen(() => {
    loginPage.form.loginSuccessfully(email, password);
  });

  andThen(() => {
    assert.equal(loginPage.navMenu.userMenu.logOut.text, 'Log out', 'Page contains logout link');
    assert.equal(currentURL(), '/projects');
  });
});

Integration

test('it renders required ui elements', function(assert) {
  this.render(hbs`{{login-form}}`);

  let $component = this.$('.login-form');
  assert.equal($component.find('form').length, 1, 'The form renders');

  let $form = $component.find('form');
  assert.equal($form.find('input#identification').length, 1, 'The identification field renders');
  assert.equal($form.find('input#password').length, 1, 'The password field renders');

  assert.equal($form.find('button#login').length, 1, 'The login button renders');
});

Unit

test('it correctly returns twitterUrl', function(assert) {
  assert.expect(1);

  let model = this.subject({ twitter: 'johndoe' });

  assert.equal(model.get('twitterUrl'), 'https://twitter.com/johndoe');
});
  twitterUrl: computed('twitter', function() {
    return `https://twitter.com/${this.get('twitter')}`;
  })
@ssriera
Copy link

ssriera commented Mar 14, 2017

✨ 🎈 I love this! I saw that same reddit post a while back- excellent write up Bill- can't wait to see the finished product.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment