Tải bản đầy đủ (.pdf) (10 trang)

Phát triển Javascript - part 5 ppsx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.35 MB, 10 trang )

ptg
1.3 Test Functions, Cases, and Suites
13
1.3.1 Setup and Teardown
xUnit frameworks usually provide setUp and tearDown methods. These are
called before and after each test method respectively, and allow for centralized
setup of test data, also known as test fixtures. Let’s add the date object as a test
fixture using the setUp method. Listing 1.12 shows the augmented testCase
function that checks if the test case has setUp and tearDown, and if so, runs
them at the appropriate times.
Listing 1.12 Implementing setUp and tearDown in testCase
function testCase(name, tests) {
assert.count = 0;
var successful = 0;
var testCount = 0;
var hasSetup = typeof tests.setUp == "function";
var hasTeardown = typeof tests.tearDown == "function";
for (var test in tests) {
if (!/^test/.test(test)) {
continue;
}
testCount++;
try {
if (hasSetup) {
tests.setUp();
}
tests[test]();
output(test, "#0c0");
if (hasTeardown) {
tests.tearDown();
}


// If the tearDown method throws an error, it is
// considered a test failure, so we don't count
// success until all methods have run successfully
successful++;
} catch (e) {
output(test + " failed: " + e.message, "#c00");
}
}
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
14
Automated Testing
var color = successful == testCount ? "#0c0" : "#c00";
output("<strong>" + testCount + " tests, " +
(testCount - successful) + " failures</strong>",
color);
}
Using the new setUp method, we can add an object property to hold the test
fixture, as shown in Listing 1.13
Listing 1.13 Using setUp in the strftime test case
testCase("strftime test", {
setUp: function () {
this.date = new Date(2009, 9, 2, 22, 14, 45);
},
"test format specifier Y": function () {
assert("%Y should return full year",
this.date.strftime("%Y") == 2009);
},
//

});
1.4 Integration Tests
Consider a car manufacturer assembly line. Unit testing corresponds to verifying
each individual part of the car: the steering wheel, wheels, electric windows, and
so on. Integration testing corresponds to verifying that the resulting car works as
a whole, or that smaller groups of units behave as expected, e.g., making sure the
wheels turn when the steering wheel is rotated. Integration tests test the sum of its
parts. Ideally those parts are unit tested and known to work correctly in isolation.
Although high-level integration tests may require more capable tools, such as
software to automate the browser, it is quite possible to write many kinds of integra-
tion tests using a xUnit framework. In its simplest form, an integration test is a test
that exercises two or more individual components. In fact, the simplest integration
tests are so close to unit tests that they are often mistaken for unit tests.
In Listing 1.6 we fixed the “y” format specifier by zero padding the re-
sult of calling date.getYear(). This means that we passed a unit test for
Date.prototype.strftime by correcting Date.formats.y. Had the lat-
ter been a private/inner helper function, it would have been an implementation
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
1.4 Integration Tests
15
detail of strftime, which would make that function the correct entry point to
test the behavior. However, because Date.formats.y is a publicly available
method, it should be considered a unit in its own right, which means that the afore-
mentioned test probably should have exercised it directly. To make this distinction
clearer, Listing 1.14 adds another format method, j, which calculates the day of the
year for a given date.
Listing 1.14 Calculating the day of the year
Date.formats = {

//
j: function (date) {
var jan1 = new Date(date.getFullYear(), 0, 1);
var diff = date.getTime() - jan1.getTime();
// 86400000 == 60 * 60 * 24 * 1000
return Math.ceil(diff / 86400000);
},
//
};
The Date.formats.j method is slightly more complicated than the previous
formatting methods. How should we test it? Writing a test that asserts on the
result of new Date().strftime("%j") would hardly constitute a unit test
for Date.formats.j. In fact, following the previous definition of integration
tests, this sure looks like one: we’re testing both the strftime method as well as
the specific formatting. A better approach is to test the format specifiers directly,
and then test the replacing logic of strftime in isolation.
Listing 1.15 shows the tests targeting the methods they’re intended to test
directly, avoiding the “accidental integration test.”
Listing 1.15 Testing format specifiers directly
testCase("strftime test", {
setUp: function () {
this.date = new Date(2009, 9, 2, 22, 14, 45);
},
"test format specifier %Y": function () {
assert("%Y should return full year",
Date.formats.Y(this.date) === 2009);
},
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg

16
Automated Testing
"test format specifier %m": function () {
assert("%m should return month",
Date.formats.m(this.date) === "10");
},
"test format specifier %d": function () {
assert("%d should return date",
Date.formats.d(this.date) === "02");
},
"test format specifier %y": function () {
assert("%y should return year as two digits",
Date.formats.y(this.date) === "09");
},
"test format shorthand %F": function () {
assert("%F should be shortcut for %Y-%m-%d",
Date.formats.F === "%Y-%m-%d");
}
});
1.5 Benefits of Unit Tests
Writing tests is an investment. The most common objection to unit testing is that
it takes too much time. Of course testing your application takes time. But the
alternative to automated testing is usually not to avoid testing your application
completely. In the absence of tests, developers are left with a manual testing process,
which is highly inefficient: we write the same throwaway tests over and over again,
and we rarely rigorously test our code unless it’s shown to not work, or we otherwise
expect it to have defects. Automated testing allows us to write a test once and run
it as many times as we wish.
1.5.1 Regression Testing
Sometimes we make mistakes in our code. Those mistakes might lead to bugs that

sometimes find their way into production. Even worse, sometimes we fix a bug but
later have that same bug creep back out in production. Regression testing helps us
avoid this. By “trapping” a bug in a test, our test suite will notify us if the bug ever
makes a reappearance. Because automated tests are automated and reproducible,
we can run all our tests prior to pushing code into production to make sure that
past mistakes stay in the past. As a system grows in size and complexity, manual
regression testing quickly turns into an impossible feat.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
1.5 Benefits of Unit Tests
17
1.5.2 Refactoring
To refactor code is to change its implementation while leaving its behavior intact. As
with unit tests, you have likely done it whether you called it refactoring or not. If you
ever extracted a helper method from one method to reuse it in other methods, you
have done refactoring. Renaming objects and functions is refactoring. Refactoring
is vital to growing your application while preserving a good design, keeping it DRY
(Don’t Repeat Yourself) and being apt to adopt changing requirements.
The failure points in refactoring are many. If you’re renaming a method, you
need to be sure all references to that method have changed. If you’re copy-pasting
some code from a method into a shared helper, you need to pay attention to such
details as any local variables used in the original implementation.
In his book Refactoring: Improving the Design of Existing Code [1], Martin
Fowler describes the first step while refactoring the following way: “Build a solid
set of tests for the section of code to be changed.” Without tests you have no reliable
metric that can tell you whether or not the refactoring was successful, and that new
bugs weren’t introduced. In the undying words of Hamlet D’Arcy, “don’t touch
anything that doesn’t have coverage. Otherwise, you’re not refactoring; you’re just
changing shit.”[2]

1.5.3 Cross-Browser Testing
As web developers we develop code that is expected to run on a vast combination of
platforms and user agents. Leveraging unit tests, we can greatly reduce the required
effort to verify that our code works in different environments.
Take our example of the strftime method. Testing it the ad hoc way involves
firing up a bunch of browsers, visiting a web page that uses the method and manually
verifying that the dates are displayed correctly. If we want to test closer to the code in
question, we might bring up the browser console as we did in Section 1.1, The Unit
Test, and perform some tests on the fly. Testing strftime using unit tests simply
requires us to run the unit test we already wrote in all the target environments.
Given a clever test runner with a bunch of user agents readily awaiting our tests,
this might be as simple as issuing a single command in a shell or hitting a button in
our integrated development environment (IDE).
1.5.4 Other Benefits
Well-written tests serve as good documentation of the underlying interfaces. Short
and focused unit tests can help new developers quickly get to know the system being
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
18
Automated Testing
developed by perusing the tests. This point is reinforced by the fact that unit tests
also help us write cleaner interfaces, because the tests force us to use the interfaces as
we write them, providing us with shorter feedback loops. As we’ll see in Chapter 2,
The Test-Driven Development Process, one of the strongest benefits of unit tests is
their use as a design tool.
1.6 Pitfalls of Unit Testing
Writing unit tests is not always easy. In particular, writing good unit tests takes
practice, and can be challenging. The benefits listed in Section 1.5, Benefits of Unit
Tests all assume that unit tests are implemented following best practices. If you write

bad unit tests, you might find that you gain none of the benefits, and instead are
stuck with a bunch of tests that are time-consuming and hard to maintain.
In order to write truly great unit tests, the code you’re testing needs to be
testable. If you ever find yourself retrofitting a test suite onto an existing application
that was not written with testing in mind, you’ll invariably discover that parts of
the application will be challenging, if not impossible, to test. As it turns out, testing
units in isolation helps expose too tightly coupled code and promotes separation of
concerns.
Throughout this book I will show you, through examples, characteristics of
testable code and good unit tests that allow you to harness the benefits of unit
testing and test-driven development.
1.7 Summary
In this chapter we have seen the similarities between some of the ad hoc testing we
perform in browser consoles and structured, reproducible unit tests. We’ve gotten
to know the most important parts of the xUnit testing frameworks: test cases, test
methods, assertions, test fixtures, and how to run them through a test runner. We
implemented a crude proof of concept xUnit framework to test the initial attempt
at a strftime implementation for JavaScript.
Integration tests were also dealt with briefly in this chapter, specifically how we
can realize them using said xUnit frameworks. We also looked into how integration
tests and unit tests often can get mixed up, and how we usually can tell them apart
by looking at whether or not they test isolated components of the application.
When looking at benefits of unit testing we see how unit testing is an investment,
how tests save us time in the long run, and how they help execute regression tests.
Additionally, refactoring is hard, if not impossible, to do reliably without tests.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
1.7 Summary
19

Writing tests before refactoring greatly reduces the risk, and those same tests can
make cross-browser testing considerably easier.
In Chapter 2, The Test-Driven Development Process, we’ll continue our explo-
ration of unit tests. We’ll focus on benefits not discussed in this chapter: unit tests
as a design tool, and using unit tests as the primary driver for writing new code.
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
This page intentionally left blank
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
2
The Test-Driven
Development Process
I
n Chapter 1, Automated Testing, we were introduced to the unit test, and learned
how it can help reduce the number of defects, catch regressions, and increase de-
veloper productivity by reducing the need to manually test and tinker with code. In
this chapter we are going to turn our focus from testing to specification as we delve
into test-driven development. Test-driven development (TDD) is a programming
technique that moves unit tests to the front row, making them the primary entry
point to production code. In test-driven development tests are written as specifica-
tion before writing production code. This practice has a host of benefits, including
better testability, cleaner interfaces, and improved developer confidence.
2.1 Goal and Purpose of Test-Driven Development
In his book, Test-Driven Development By Example[3], Kent Beck states that the goal
of test-driven development is Clean code that works. TDD is an iterative develop-
ment process in which each iteration starts by writing a test that forms a part of
the specification we are implementing. The short iterations allow for more instant

feedback on the code we are writing, and bad design decisions are easier to catch.
By writing the tests before any production code, good unit test coverage comes with
the territory, but that is merely a welcome side effect.
21
From the Library of WoweBook.Com
Download from www.eBookTM.com
ptg
22
The Test-Driven Development Process
2.1.1 Turning Development Upside-Down
In traditional programming problems are solved by programming until a concept is
fully represented in code. Ideally, the code follows some overall architectural design
considerations, although in many cases, perhaps especiallyin the worldof JavaScript,
this is not the case. This style of programming solves problems by guessing at what
code is required to solve them, a strategy that can easily lead to bloated and tightly
coupled solutions. If there are no unit tests as well, solutions produced with this
approach may even contain code that is never executed, such as error handling
logic, and edge cases may not have been thoroughly tested, if tested at all.
Test-driven development turns thedevelopment cycle upside-down.Rather than
focusing on what code is required to solve a problem, test-driven development starts
by defining the goal. Unit tests form both the specification and documentation for
what actions are supported and accounted for. Granted, the goal of TDD is not
testing and so there is no guarantee that it handles edge cases better. However,
because each line of code is tested by a representative piece of sample code, TDD
is likely to produce less excessive code, and the functionality that is accounted for
is likely to be more robust. Proper test-driven development ensures that a system
will never contain code that is not being executed.
2.1.2 Design in Test-Driven Development
In test-driven development there is no “Big Design Up Front,” but do not mistake
that for “no design up front.” In order to write clean code that is able to scale across

the duration of a project and its lifetime beyond, we need to have a plan. TDD
will not automatically make great designs appear out of nowhere, but it will help
evolve designs as we go. By relying on unit tests, the TDD process focuses heavily on
individual components in isolation. This focus goes a long way in helping to write
decoupled code, honor the single responsibility principle, and to avoid unnecessary
bloat. The tight control over the development process provided by TDD allows for
many design decisions to be deferred until they are actually needed. This makes it
easier to cope with changing requirements, because we rarely design features that
are not needed after all, or never needed as initially expected.
Test-driven development also forces us to deal with design. Anytime a new
feature is up for addition, we start by formulating a reasonable use case in the form
of a unit test. Writing the unit test requires a mental exercise—we must describe the
problem we are trying to solve. Only when we have done that can we actually start
coding. In other words, TDD requires us to think about the results before providing
the solution. We will investigate what kind of benefits we can reap from this process
From the Library of WoweBook.Com
Download from www.eBookTM.com

×