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.”)
- Test-Driven Development with Python by Harry J.W. Percival
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 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.
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.
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';
- Use Ember Test Selectors by Simplabs
In a template:
<h1 data-test-post-title>{{post.title}}</h1>
In your tests:
assert.equal(find(testSelector('post-title')).text(), 'blog post');
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.
-
Testing User Interaction
-
Testing Actions
-
Stubbing Services
-
See testing/testing-components guide.
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 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.
- See unit-testing-basics guide.
- And: testing-controllers, testing-routes, testing-models
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".
- Test incoming query messages by making assertions about what they send back
- Test incoming command messages by making assertions about direct public side effects
- 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.
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.
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.
- Travis CI, https://github.com/travis-ci/travis-web
- Code Corps, https://github.com/code-corps/code-corps-ember
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…
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');
});
});
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');
});
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')}`;
})
✨ 🎈 I love this! I saw that same reddit post a while back- excellent write up Bill- can't wait to see the finished product.