(Test Driven Development)
TDD reduces production bug density by 40–80%.
TDD - The Art of Fearless Programming
I’m building a function that does x, and it needs a function signature that takes y and returns z.
The point of TDD is that it forces you to have a direction in mind before you start charging into the fray, and having a direction in mind leads to better designs.
One of the great benefits of TDD is that it can help you refactor when you need to.
Work with your team (if you have one) to decide what needs tests and what kind of tests are needed.
Test the individual functionality of the component you are coding.
Your tests are completely focused on the
singular piece of code you're writing.
Here you test how your code fits in with the
other components in your application.
In front end dev, this also means how
your code can be interacted with.
Think of how it integrates with the rest of the app.
Comparing two screenshots and visually highlighting the difference between them is visual regression testing.
This is a great way to test for changes or regressions in the layout of a webpage, especially if there are subtle differences in your layout depending on viewport size.
module("User Registration: Validation");
test("validate user input", function() {
var emailIsValid = function_to_validate_email(user_input);
ok(emailIsValid, "Validation successful");
};
test("verify unique email in database", function() {
var emailExists = function_to_search_db(user_input);
ok(!emailExists, "Email is unique and can be added to db.");
};
module("User Registration: Save to DB");
test("verify user record is saved to DB", function() {
// This assumes DB.save() returns a truthy/falsey value
ok(DB.save(user_input), "Record saved successfully");
};
describe("User Registration", function() {
describe("While validating the registration info", function() {
it("should make sure the email address is valid", function() {
expect(function_to_validate_email(user_input)).toBeTrue();
});
it("should verify the email doesn't already exist", function() {
expect(function_to_search_db(user_input)).toBeEmpty();
});
});
describe("While creating the user's database record", function() {
it("should save successfully in the database", function() {
expect(function_to_save_user(user_input)).toBeTruthy();
});
});
});
User Registration: Validation
- validate user input
1. Validation Successful
- verify email does not exist in db
1. Email is unique and can be added to DB.
User Registration: Save to DB
- verify user record is saved successfully
1. Record saved successfully.
Describe User Registration
While validating the registration info
- it should make sure the email address is valid
- it should verify the email doesn't already exist
...
While creating the user's database record
- it should save successfully in the database
...
(The heart of TDD)
Unit testing is the process of breaking down your application to the smallest possible functions, and creating a repeatable, automated test that should continually produce the same result.
With unit tests, every system function can be verified before a single line of code is even merged into the master branch.
Create smaller functions that can be used throughout our entire application.
A function that determines the cost to ship a product to a customer can break down to 3 separate functions:
Look up distribution center nearest to given address.
Calculate a distance between two addresses.
Evaluate shipping cost for a given distance.
import test from 'ava';
test('Calculate Shipping', t => {
t.is(calculateShipping(24), 4);
t.is(calculateShipping(99), 5);
t.is(calculateShipping(999), 6);
t.is(calculateShipping(1000), 7);
});
As long as calculateShipping(24) returns a value of 4 (which it will here), our test will pass.
function calculateShipping(distance) {
switch (distance) {
case (distance < 25):
shipping = 4;
break;
case (distance < 100):
shipping = 5;
break;
case (distance < 1000):
shipping = 6;
break;
case (distance > = 1000):
shipping = 7;
break;
}
return shipping;
}
A very simple test of AddClass::add() from the examples module.
class AddClassTest extends UnitTestCase {
/**
* Very simple test of AddClass::add().
*
* This is a very simple unit test of a single method. It has
* a single assertion, and that assertion is probably going to
* pass. It ignores most of the problems that could arise in the
* method under test, so therefore: It is not a very good test.
*/
public function testAdd() {
$sut = new AddClass();
$this->assertEquals($sut->add(2, 3), 5);
}
}
A simple addition method with validity checking from the examples module.
class AddClass {
/**
* A simple addition method with validity checking.
*
* @param int|float $a
* A number to add.
* @param int|float $b
* Another number to add.
*
* @return numeric
* The sum of $a and $b.
*
* @throws \InvalidArgumentException
* If either $a or $b is non-numeric, we can't add, so we throw.
*/
public function add($a, $b) {
// Check whether the arguments are numeric.
foreach (array($a, $b) as $argument) {
if (!is_numeric($argument)) {
throw new \InvalidArgumentException('Arguments must be numeric.');
}
}
return $a + $b;
}
}
(The heart of BDD)
Functional testing takes a more formal approach to manual testing. It involves creating test use cases to verify specific functionality.
Functional tests allow you to concentrate your manual testing on the aspects of web design that are hard to automate (i.e. multi-browser testing and client-side performance.)
As a user I want to be able to replace my card with a new one with new artwork if my current artwork is unavailable.
@api @en1691 @selfservice @lost
Scenario: I do not want to replace a lost card without a balance.
Given the current payment processor is "fis"
And Drupal has "fis_mock_balance" set to "0.00"
And the available acquisition subprograms are "bancorp_mc"
And an existing cardholder:
| rpid | 13370009 |
| email | john.smith@example.com |
| status | active |
And I am logged in with email "john.smith@example.com"
When I am at "/"
When I click "Account"
And I click "Manage Cards"
And I click "Lost / Stolen / Damaged Card"
Then I should see "Replace/Suspend card ending in 0009"
When I click "Lost / Stolen"
Then I should see "Once you click on one of the buttons below your card ending in 0009"
When I press "Close This Card"
Then I should see "Your card has been closed."
And I should not see "You still have funds on your account."
And there is a watchdog message like 'Changing status of @rpid to @status because "@reason"'
Allows us to make visual comparisons between the correct (baseline) versions of our site and versions in development or just about to be deployed (new).
Component-based diffing (i.e. Backstop.js)
CSS Unit Testing (i.e. Quixote)
Headless Browser Driven (i.e. Gemini)
Desktop Browser Driven (i.e. Selenium with Gemini)
Command-line Comparison (i.e. PhantomCSS)
# (required) The engine to run Wraith with. Examples: 'phantomjs', 'casperjs', 'slimerjs'
browser: "phantomjs"
# (required) The domains to take screenshots of.
domains:
prod: "http://www.card.com"
dev: "http://dev.card.docker"
# (required) The paths to capture. All paths should exist for both of the domains specified above.
paths:
home: /
atm-locator: /atm-locator
add-money: /add-money
# (required) Screen widths (and optional height) to resize the browser to before taking the screenshot.
screen_widths:
- 320
- 600x768
- 768
- 1024
- 1280
# (optional) JavaScript file to execute before taking screenshot of every path. Default: nil
before_capture: 'javascript/wait--phantom.js'
# (required) The directory that your screenshots will be stored in
directory: 'shots'
# (required) Amount of fuzz ImageMagick will use when comparing images. A higher fuzz makes the comparison less strict.
fuzz: '20%'
# (optional) The maximum acceptable level of difference (in %) between two images before Wraith reports a failure. Default: 0
threshold: 5
# (optional) Specify the template (and generated thumbnail sizes) for the gallery output.
# gallery:
template: 'basic_template' # Examples: 'basic_template' (default), 'slideshow_template'
thumb_width: 200
thumb_height: 200
# (optional) Choose which results are displayed in the gallery, and in what order. Default: alphanumeric
# Options:
# alphanumeric - all paths (with or without a difference) are shown, sorted by path
# diffs_first - all paths (with or without a difference) are shown, sorted by difference size (largest first)
# diffs_only - only paths with a difference are shown, sorted by difference size (largest first)
# Note: different screen widths are always grouped together.
mode: diffs_first
Unit + Functional Tests
BackstopJS
Quixote
Gemini
CasperJS
Diffux
etc
You need to make sure your tests are as deterministic as possible.
(no randomness is involved)
Expose ways to set content using either mocked or fixed content. This way you are testing the changes to your code as opposed to changes to the content itself.
If you are comparing the test version of your site against the live version, and they both rely on the same dependency (i.e. services that is consumed over HTTP), errors caused by the common dependency will be replicated and will not be noticed.
It’s better to test parts of your UI in isolation. Having one single test that can break because of many different things in a page can make it hard to work out what is going wrong. Finer granularity can help with automated test reports, too.
Well-designed applications are highly abstract and under constant pressure to evolve; without tests these applications can neither be understood nor safely changed.
~Sandi Metz
A well-designed application with a carefully crafted test suite is a joy to behold and a pleasure to extend. It can adapt to every new circumstance and meet any unexpected need.
~Sandi Metz
Slides: http://bit.ly/frontend-tdd