Front End TDD
(Test Driven Development)
Follow along:
bit.ly/frontend-tdd-live
Ryan McVeigh
Impromptu Survey
Why Test?
Why Test First?
The Science of TDD
The evidence says:
- TDD can reduce bug density.
- TDD can encourage more modular designs (enhancing software agility/team velocity).
- TDD can reduce code complexity.
TDD reduces production bug density by 40–80%.
TDD - The Art of Fearless Programming
5 Common Misconceptions About TDD & Unit Tests
Misconception #1
TDD is too Time Consuming. The Business Team Would Never Approve
TDD can:
- Improve developer productivity (long term)
- Reduce customer abandonment
- Increase the viral factor of your application (i.e., user growth)
- Reduce the costs of customer service
Misconception #2
You Can’t Write Tests Until You Know the Design, & You Can’t Know the Design Until You Implement the Code
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.
Misconception #3
You Have to Write All Tests Before You Start the Code
-
Write one test
-
Watch it fail
-
Implement the code
-
Watch the test pass
-
Repeat
Misconception #4
Red, Green, and ALWAYS Refactor?
“Perfect is the enemy of good.” ~ Voltaire
One of the great benefits of TDD is that it can help you refactor when you need to.
Misconception #5
Everything Needs a Test
Healthy test suites will recognize that there are three major types of software tests that all play a role, and your test coverage will create a balance between them.
Work with your team (if you have one) to decide what needs tests and what kind of tests are needed.
Focus on Testing
There are a lot of different types
Unit Testing
Test the individual functionality of the component you are coding.
Your tests are completely focused on the
singular piece of code you're writing.
Functional Testing
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.
Visual Regression Testing
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.
Holy Trinity of Frontend Testing
Others
- Component Integration
- Performance Testing
- Systems Integration
- Stress Testing (load testing)
- Quality Assurance Testing
- User Acceptance Testing
- ... and many more ...
Testing Styles:
-
TDD - Test Driven Development
-
BDD - Behavior Driven Development
TDD Style (QUnit):
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");
};
BDD Style (Jasmine):
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();
});
});
});
TDD is Terse
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.
Tests show clear linear test progression.
BDD is Narrative
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
...
Tests are expressed in behavioral terms
Unit Testing
(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.
The Unit
“Do one thing, and do it well”
Create smaller functions that can be used throughout our entire application.
Unit Test Example
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.
Unit Test (AVA)
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.
Calculate Shipping
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;
}
How 'bout some PHPUnit?
Example PHPUnit
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);
}
}
Example PHPUnit
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;
}
}
Functional Testing
(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.)
Functional Testing Example
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.
Behat (with Selinium + Phantomjs)
@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"'
Visual Regression Testing
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).
The Flavors
- Page-based Diffing (i.e. Wraith)
-
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)
Wraith (with Phantomjs)
# (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
Result
How Much Coverage Is Enough?
Test everything, and your development process can get bogged down.
Don’t test enough, and you risk regressions slipping through.
-
Start out small and let it grow with time.
-
When in doubt write the test and consult with your team during code review.
-
Not every feature requires the same amount of test coverage. But the assumption is that every story/feature starts with tasks for test coverage.
Here are a few things to remember when you start planning test coverage for your application
- Tests are written while the site is being built, or even before actual code is written.
- They are living code, committed with or next to the system repository.
- All tests must pass before any code is merged into the master branch.
- Running the suite on the master branch should always return a passing response.
Automated QA (Probo.CI)
Okay, but what kind of tests should I write??
Why Not Both
Unit + Functional Tests
Okay, but can this be automated?
Testing tools have entered the building!
Where to start?
In your IDE
With a task runner
Testing Frameworks
Unit Testing Frameworks
- PHPUnit
- AVA
- NodeUnit
- QUnit
- Mocha
- Codeception
- SimpleTest
- Karma
- JSUnit
- etc
Functional Testing Frameworks
- Selenium
- Behat
- CasperJS
- Jasmine
- Mocha
- AVA
- Expect.js
- etc
Visual Regression
- Wraith
- PhantomCSS
- Drulenium
- dpxdt (pronounced Depicted)
-
BackstopJS
-
Quixote
-
Gemini
-
CasperJS
-
Diffux
-
etc
Things to keep in mind
Be wary of false positives
You need to make sure your tests are as deterministic as possible.
(no randomness is involved)
Test consistently
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.
Watch out for common dependencies
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.
Be deliberate in your tests
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.
Organize
IDENTIFYING WHEN TESTING HAS BECOME A PROBLEM
- Sections of your codebase have had tests run against them more times than they have lines of code.
- Testing takes longer than the development.
- Deployments used to be easy, now they are hard and take longer.
- Team members don’t like running the tests.
- Team members start disabling tests to get the build to pass.
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
Resources
Frontend Architecture for Design Systems
by Micah Godbolt
Smashing Book 5
Testing and Debugging Responsive Web Design
by Tom Maslen
Practical Object-Oriented Design in Ruby
by Sandi Metz
Ryan McVeigh
Slides: http://bit.ly/frontend-tdd
Front End TDD
By Ryan McVeigh
Front End TDD
- 2,710