RUNNING JASMINE UNIT TESTS

Một phần của tài liệu reliable javascript how to code safely in the world s most dangerous language spencer richards 2015 07 13 Lập trình Java (Trang 80 - 98)

While there are automated test runners for Jasmine unit tests, such as Karma (http://karma-runner.github.io/), in this book you’ll leverage Jasmine’s ability to run on-demand in the browser. To do so, simply create a plain HTML file and add references to the Jasmine library

JavaScript and CSS files. Then, add a reference for each of the JavaScript files containing code you would like to test. Finally, add a script reference for each of the Jasmine unit test files that contains tests you would like to execute.

The HTML file to test the code from Listing 2-1 using the tests in Listing 2-2 looks like this:

<!DOCTYPE html>

<html>

<head>

<!-- Jasmine Library Files -->

<link data-require="jasmine@*" data-semver="2.0.0"

rel="stylesheet"

href="http://cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css"

/>

<script data-require="jasmine@*" data-semver="2.0.0"

src="http://cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js">

</script>

<script data-require="jasmine@*" data-semver="2.0.0"

src="http://cdn.jsdelivr.net/jasmine/2.0.0/jasmine- html.js">

</script>

<script data-require="jasmine@*" data-semver="2.0.0"

src="http://cdn.jsdelivr.net/jasmine/2.0.0/boot.js">

</script>

<!-- Code Under Test -->

<script src="TestFrameworks_01.js"></script>

<!-- Unit Tests -->

<script src="TestFrameworks_01_tests.js"></script>

</head>

</html>

All of our samples retrieve the Jasmine files from a content distribution

network (CDN), but it is also possible to download the files and include them from your local computer.

You’ve satisfied your team’s unit test requirement, so you check in the code and confidently move on to the next piece of functionality.

A few hours (or maybe days or weeks) later, you get an e-mail from Charlotte, a fellow team member, who is integrating your createReservation function into another area of the application. When she runs her suite of integration tests, all of the tests that exercise the createReservation function fail.

“Impossible,” you respond, “All of the unit tests for that function pass!”

Closer inspection reveals that the unit tests are incorrect. The specification says that the attribute names of the returned reservation object should be

passengerInformation and flightInformation, and Charlotte wrote her code expecting those attributes to be present. Unfortunately, in our hasty

implementation of createReservation we used the attribute names

passengerInfo and flightInfo.

Because the tests were written according to the function’s implementation rather than its specification, they verify the actual—incorrect—behavior of the function rather than the expected behavior of the function. Had they been

written first, based solely on the specification, the attribute names probably would have been correct the first time.

We won’t dispute that the same mistake could happen if the function had been written using TDD, which would base the tests on the specification rather than the existing code. Our experience has shown that it is far less likely, however.

NOTE When working with existing code without unit tests, it is usually necessary to write tests that verify actual functionality. Doing so allows you to refactor the code while ensuring that its outward functionality does not change.

Identifying Incorrect Code

TDD identifies defects in code at the earliest possible time: the moment after they’re created. When following TDD, a test is written to verify a small piece of functionality, and then the functionality is implemented with the

minimum amount of code possible.

Returning now to the createReservation function, you will see how a different outcome is assured by writing the tests first. As a reminder, the specification for the function as described earlier in the chapter is:

Given a passenger object and a flight object, createReservation will return a new object with the passengerInformation property set to the provided passenger object and the flightInformation property set to the provided flight object.

Listing 2-3 shows the first test to verify that the passengerInformation

property is properly assigned.

LISTING 2-3: First TDD unit test for createReservation (code filename: Test

Frameworks\TestFrameworks_02_tests.js)

describe('createReservation(passenger, flight)', function(){

it('assigns the provided passenger to the passengerInfo property',

function(){

var testPassenger = {

firstName: 'Pete', lastName: 'Mitchell' };

var testFlight = { number: '3443', carrier: 'AceAir',

destination: 'Miramar, CA' };

var reservation = createReservation(testPassenger, testFlight);

expect(reservation.passengerInformation).toBe(testPassenger);

});

});

Listing 2-4 shows your minimum implementation of createReservation that will cause the test to pass.

LISTING 2-4: Initial TDD implementation of createReservation (code filename: Test Frameworks\TestFrameworks_02.js)

function createReservation(passenger, flight){

return {

passengerInfo: passenger, flightInformation: flight };

}

You then immediately execute the unit test and it fails (Figure 2.2). How did that happen?

Figure 2.2

Ah! The attribute name in the returned object was incorrect; it was called

passengerInfo instead of passengerInformation. You quickly change the name of the attribute to the specified passengerInformation, and now your test passes (Figure 2.3). Note that the figure also reflects a change to the it statement: You changed the it statement to also indicate that the

passengerInformation property is being tested.

Figure 2.3

You can then follow the same process for assignment of the

flightInformation attribute, yielding a createReservation function that is verified correct via unit tests.

The error of incorrectly naming an attribute in the returned object once again made its way into the implementation of the createReservation function.

This time, however, you wrote your test first and based the test on the

specification. This allowed you to immediately identify and address the error rather than waiting hours (or days, or weeks) for another developer running integration tests to alert you to the problem.

For a trivial function such as createReservation, this piecewise creation of tests and addition of functionality admittedly feels like a bit of overkill. It is easy to imagine other cases, however, where the iterative process of TDD

could end up saving a significant amount of debugging time.

Suppose that you have to write a function that performs a multitude of

computations on an array of data. You attempt to implement a large portion of the function all at once, and that portion contains an off-by-one error such that you omit the last piece of data from the computations.

When you verify the output of your function, the computed value will be incorrect, but you won’t know why it’s incorrect. It could be a mathematical error in one of the computations, or perhaps you’re not handling a case of numeric overflow. Had you written a simple test early on to verify that each and every element of the array is involved in the computation, you would have immediately caught the off-by-one error and likely would have had a working solution much sooner.

Designing for Testability

Writing tests first makes the testability of your code a primary concern rather than an afterthought. In our experience working with developers at all skill levels, there is a direct correlation between how easy code is to test and how well that code is tested. Additionally, we’ve found that code that is easy to test tends to be easier to maintain and extend. As we proposed in the first chapter, code that follows the principles of SOLID development lends itself quite well to testing. If you make testability a goal of your designs you will tend to write SOLID code.

For example, suppose every reservation created with the createReservation

function should be submitted to a web service to be stored in a database.

If you’re not practicing TDD, you may simply extend createReservation by adding a call to jQuery.ajax(...) which sends the reservation to a web

service endpoint via asynchronous JavaScript and XML (AJAX). The addition of this one simple function call, though, has increased the number of

responsibilities the humble little createReservation function has, and has increased the effort required to properly unit test it.

NOTE Testing interactions with web services is not trivial, but it is both possible and necessary when building a reliable code base.

You’ve already added the jQuery.ajax(...) call, you already have tests for some of the things the createReservation function does, and maybe you

manually tested and verified that the reservation ends up in the database. It’s easy to decide that it’s just too much effort to write unit tests for that one little bit of functionality and move on to your next task. We’ve seen it happen many times, and admit to doing it many times ourselves.

Assuming you have committed yourself to TDD, instead of steaming ahead and updating createReservation, you would write a test to verify the new functionality. The first test verifies that the reservation is sent to the correct web service endpoint.

You probably wouldn’t get much further than defining the behavior before you would ask yourself the question, “Should createReservation be

responsible for communicating with a web service?”

describe('createReservation(passenger, flight)', { // Existing tests

it('submits the reservation to a web service endpoint', function(){

// should createReservation be responsible for // communicating with a web service?

});

});

The answer to the question is: No, most likely not. If one doesn’t exist

already, you would benefit from creating (and testing!) an object with the sole responsibility of web service communication.

By maximizing the testability of your code, you are able to identify violations of the SOLID principles. Many times, as in the example above, you can avoid violating them altogether.

Writing the Minimum Required Code

To review the basic TDD workflow, you write a test that will fail in order to verify a small piece of functionality, and then you write the minimum amount of code possible to make that new test pass. You then change the internal implementation details of the code under development, known as refactoring, to remove duplication.

Between only adding the minimum lines of code, and then refactoring to remove duplicate code, you can be certain that at the end of the process you have added the fewest lines of code possible. This is perfect, because there can be no defects in code you don’t write!

Safe Maintenance and Refactoring

Practicing TDD guarantees that you will have a robust unit test suite for the production code in your project. Up-to-date unit tests are an insurance policy against future regression defects. A regression defect is a defect that appears in code that worked correctly at some time in the past, but is no longer

working properly; the quality and reliability of the code has regressed.

Like any other insurance policy, there’s a recurring cost that seems like a burden without benefit. In the case of unit-testing, the recurring cost is the development and maintenance of the unit test suite. Also like any other

insurance policy, there comes a point when you are relieved that you paid that recurring cost. If you’re a homeowner, it may be a violent storm that knocks a tree onto your house causing a significant amount of damage (as happened to one of us).

You will feel a similar sense of relief when it comes time to extend or

maintain production code that has a comprehensive unit test suite. You can make changes to a portion of the code (still following TDD, of course) and be confident that you have not unintentionally changed the behavior of other portions of the code.

Runnable Specification

A robust unit test suite, like one generated when practicing TDD, also acts as a runnable specification for the code that the suite tests. Jasmine, the unit test framework that we’ll introduce in a following section (and use

throughout this book) uses a behavior-based test organization. Each individual test, or specification as Jasmine refers to them, begins with a natural-text statement about the behavior that the test is exercising and verifying. The default Jasmine test result formatter shows these statements to us for each test.

Once again using the createReservation function as an example, you can see that it’s possible to read the output of the Jasmine unit tests to get a complete picture of how the function behaves. It isn’t necessary to read the

createReservation function to determine what it’s doing; the unit tests tell you! The sort of “runnable documentation” that unit tests provide is

invaluable when adding developers to a project, or even when revisiting code that you wrote in the past.

Current Open-Source and Commercial Frameworks

While Jasmine is the JavaScript test framework that we prefer, it isn’t the only one out there. This section explores two popular alternatives: QUnit and D.O.H.

QUnit

The open-source QUnit framework was developed by the team that wrote the jQuery, jQuery UI, and jQuery Mobile JavaScript frameworks. QUnit may be run within non-browser environments, such as the node.js or Rhino

JavaScript engines. QUnit tests may also be executed in the browser after including the requisite library JavaScript and CSS files in an HTML file.

Defining a QUnit test is a low-friction affair:

QUnit.test("This is a test", function(assert){

assert.equal(true, true, "Oh no, true is not true!");

});

The only argument to a QUnit test function is a reference to the QUnit

assertion object, which exposes eight assertions—including equal, as you can see in the preceding code snippet—for use within tests.

Tests can be grouped via the QUnit.module function, which causes the tests that follow to be grouped in the test results. All the tests that follow are in the module until another QUnit.module call is encountered or the end of the file is reached. We prefer Jasmine’s nesting of tests within a suite because we find it more explicit and intuitive.

QUnit.module("module 1");

QUnit.test("I'm in module 1", function(assert){

// Test logic });

QUnit.module("module 2");

QUnit.test("I'm in module 2", function(assert){

// Test logic });

You can find out more about QUnit at http://qunitjs.com/. D.O.H.

D.O.H., the Dojo Objective Harness, was created to help the creators and maintainers of the Dojo JavaScript Framework. D.O.H. does not have any

dependencies on Dojo, however, so it may be used as a general-purpose JavaScript testing framework.

Like Jasmine and QUnit, D.O.H. supports browser-based test execution and non-browser–based execution (such as within the node.js or Rhino

JavaScript engines).

D.O.H. unit tests are defined using the register function of the doh object.

The register function accepts an array of JavaScript functions, which define simple tests, or objects, which define more complex tests that include setup and tear down (analogous to Jasmine’s beforeEach and afterEach, which you will see in the next section).

doh.register("Test Module", [ function simpleTest(){

doh.assertTrue(true) },

{

name: "more complex", setup: function(){

// code to set up the test before it runs },

runTest: function(){

doh.assertFalse(false);

},

tearDown: function(){

// Code to clean up after the test executes }

} ]);

D.O.H. provides four built-in assertions (such as assertFalse).

While we enjoy the framework’s name (D’oh!), we find the Jasmine syntax for test organization and definition to be more clear and expressive.

You can explore the D.O.H. framework at

http://dojotoolkit.org/reference-guide/1.10/util/doh.html#util-doh. NOTE Unlike the Jasmine framework, neither QUnit nor D.O.H place library functions in the global scope; they’re accessed via the QUnit and

doh objects, respectively.

Introducing Jasmine

Jasmine is a library for creating JavaScript unit tests in a behavior-driven development (BDD) style.

NOTE BDD? Aren’t we doing TDD? Behavior-driven development and test-driven development are not mutually exclusive. Behavior-driven development uses natural-language descriptions to define the

functionality, or behavior, that a particular unit test is exercising. We think that defining tests in this way helps us write tests that describe what the code we’re writing should do, rather than how it does what it does. Also, as we noted previously, tests that are defined and organized in a behavior-driven manner have the benefit of generating a

specification of functionality that is described in plain terms.

In this section, we’ll describe the basics of the framework, but we highly recommend that you visit the Jasmine homepage at

http://jasmine.github.io where you can find the library’s documentation—

which is a runnable Jasmine test suite—for in-depth exposure.

Suites and Specs

Jasmine test suites are defined using the global describe function. The

describe function accepts two arguments:

A string, usually one that describes what is being tested A function, containing the implementation of the test suite

Test suites are implemented using specs, or individual tests. Each spec is defined using the global it function. Like the describe function, the it function takes two arguments:

A string, usually one that describes the behavior being tested

A function containing at least one expectation: an assertion that a state of the code is either true or false

Test suite implementations may also make use of the global beforeEach and

afterEach functions. When included within a suite implementation, the

beforeEach is executed before each of the tests in the suite. Conversely, the

afterEach function is executed after each of the tests in the suite. The

beforeEach and afterEach functions are useful for performing common setup and teardown, reducing duplication within a test suite.

NOTE Test suites should be SOLID and DRY, too!

In Listing 2-2, there were two tests that had exactly the same setup step. We noted at the time that we’d show how to remove this blatant violation of the DRY principle. Listing 2-5 shows how to do that using the beforeEach

function.

LISTING 2-5: Jasmine’s beforeEach and afterEach (code filename: Test Frameworks\TestFrameworks_03_tests.js)

describe('createReservation(passenger, flight)', function(){

var testPassenger = null, testFlight = null,

testReservation = null;

beforeEach(function(){

testPassenger = { firstName: 'Pete', lastName: 'Mitchell' };

testFlight = { number: '3443', carrier: 'AceAir',

destination: 'Miramar, CA' };

testReservation = createReservation(testPassenger, testFlight);

});

it('assigns passenger to the passengerInformation property', function(){

expect(testReservation.passengerInformation).toBe(testPassenger);

});

it('assigns flight to the flightInformation property', function(){

expect(testReservation.flightInformation).toBe(testFlight);

});

});

For completeness, Listing 2-6 shows the implementation of

createReservation that allows the refactored unit tests to pass.

LISTING 2-6: Complete implementation of createReservation (code filename: Test Frameworks\TestFrameworks_03.js)

function createReservation(passenger, flight){

return {

passengerInformation: passenger, flightInformation: flight

};

}

Expectations and Matchers

Each of the tests contains an expect statement. Here’s the expect from the first createReservation unit test:

expect(testReservation.passengerInformation).toBe(testPassenger);

The expect function takes the actual value generated by the code under test, in this case testReservation.passengerInformation, and compares it with the value that is expected. The expected value in the first unit test is

testPassenger.

The comparison between the actual value and the expected value is

performed using a matcher function. Matchers return either true to indicate that the comparison was successful, or false to indicate the comparison was not successful. A spec that contains one or more expectations with an

unsuccessful matcher is considered failing. Conversely, a spec that contains only expectations with successful matchers is considered passing.

The example above used the toBe matcher, meaning—you guessed it—you expect testReservation.passengerInformation to be the same object as testPassenger.

Jasmine includes many built-in matchers, but if it doesn’t have the exact matcher you need, Jasmine supports creating custom matchers. Creating custom matchers can be an excellent way to DRY out the test code.

NOTE There are also matcher libraries, such as jasmine-jquery

Một phần của tài liệu reliable javascript how to code safely in the world s most dangerous language spencer richards 2015 07 13 Lập trình Java (Trang 80 - 98)

Tải bản đầy đủ (PDF)

(696 trang)