This article describes how to compile maintainable JavaScript. We gradually add new features to an example throughout the full text and follow the following simple rules: Write A unit test and let it pass. Each test serves as a quality feedback loop and creates a security protection network and executable document for those who want to modify the product code. Through simple and failed tests, we can ensure that all functions are covered by tests. We also avoid the high cost of rewriting the code before testing. Taking into account the fact that JavaScript developers are easy to get stuck and difficult to extricate themselves, this is especially valuable-just consider the number of global variables between the dom api and the Javascript language itself.
The example throughout the full text is the three-axis tiger machine in the casino. Each axis has five possible states, which are represented by images. When the play button of the tiger machine is pressed, each axis is given a random state. The balance of the tiger machine increases or decreases based on whether the three axes are in the same State.
Our tools include stubs, mock objects, and a tiny bit of dependency injection. We use jsunit to run unit tests and a javascript mock object library called jsmock. Integration testing-supplemented by unit testing, is beyond the scope of this article. This does not mean that integration testing is not important-just because we want faster feedback, rather than slower and more comprehensive feedback from tools like selenium and watir.
Jsunit, a javascript unit test framework
Jsunit is an open source unit testing framework for JavaScript. It was inspired by JUnit and fully written in JavaScript. As the most popular JavaScript unit testing framework, it also provides ant tasks that make it easy for developers to run test suites when building on continuous integration servers. Continuous integration is another important practice. Its combination with TDD is a "strong guarantee" for quality, but this is beyond the scope of this article.
Let's start with test runner of jsunit. Test runner is a common HTML and JavaScript web page, which means that your unit test can be run directly in the browser or the browser you want to support. Decompress the jsunitdownloaded file and you will find testrunner.html in the root directory. You don't need to access it through the Web server-you just need to load it through the file system for browsing.
The most important control of test runner is the file input column at the top of the page. This control is intended to obtain a path pointing to the test page or test page suite. Now let's look at a simple example on the jsunit test page.
Jsunit has many similarities with other xunit frameworks. As expected, test runner loads the test page and calls each test function. Each test function is called between setup and teardown. The setup function provides the tester with the opportunity to construct the test fixture here ). The test fixture is used to prepare all the test states on the page. The teardown function provides the tester with another opportunity to clear or reset the test fixture.
However, compared with other xunit frameworks, jsunit has a slightly different test lifecycle. Each test page is loaded into a separate window to prevent application code from overwriting the test framework code through an open class. In each loaded window, all unit test functions are called. PageNoReload each test function. On the other hand, in JUnit, the test page is equivalent to a test case. Test runner will generate a separate test case instance for each test method. In other words,
Jsunit loads the test page with n test functions, which only takes 1 time
JUnit creates a test case with n test methods, which requires n times
Javascript developers are more likely to fall into the "One trick, one mistake, all losses" situation, because changes to the test page status will affect the subsequent test results. Java developers are not at this risk when changing the state of the test case object. Why does jsunit do this, instead of simply re-loading the test page for each test? This is because re-creating the dom for each test function in the test suite consumes performance. Fortunately, JavaScript developers do not have to worry too much about the negative effects of global status changes. On program platforms such as JVM and CLR, modifying static variables affects all subsequent tests in the test suite, not just the tests of the same test case.
Jsunitcore. js scripts must be embedded in all test pages. This important file is located in the app directory after the jsunit download file is decompressed. It contains a set of assertion functions, which have the same behavior as other xunit frameworks. A subtle difference is that JavaScript has two equal symbols. One is the equal (=) operator, and the other is the third (=) operator. For example, the first expression below is true, and the second is false:
0 = false
0 = false
Why? The equality operator is not as strict as the third-class operator. It allows the runtime to perform type conversion on the first Boolean expression. Therefore, it is not difficult for new users to understand that the following assertions will pass:
Assertequals (false, 0 );
In fact, this assertion will fail, because the asserted functions provided by the jsunit framework use stricter third-class operators instead of equal operators for all comparisons. By avoiding equal operators, jsunit can avoid many tests that seem to be true or false.
Stubs vs. mocks
Let's take a look at stubs and mock objects through the example of the tiger machine. Since this unit test focuses on a single object, we create a tiger machine and treat it as a tested system. Now let's write a simple test to generate a tiger machine.
function testRender() { var buttonStub = {}; var balanceStub = {}; var reelsStub = [{},{},{}]; var randomNumbers = [2, 1, 3]; var randomStub = function(){return randomNumbers.shift();}; var slotMachine = new drw.SlotMachine(buttonStub, balanceStub, reelsStub, randomStub); slotMachine.render(); assertEquals('Pay to play', buttonStub.value); assertTrue(buttonStub.disabled); assertEquals(0, balanceStub.innerHTML); assertEquals('images/2.jpg', reelsStub[0].src); assertEquals('images/1.jpg', reelsStub[1].src); assertEquals('images/3.jpg', reelsStub[2].src);}
The testrender function uses the stub of two DOM elements, injects them into the constructor of the tested system, and calls the render method. At the end of the test, assert the expected results of the render method. Note that by using the stub of the DOM element, we can test the result of the render method without actually doing anything, which may cause other tests on the test page to fail. This method has its own advantages and disadvantages with the use of real DOM elements. Using Real DOM elements makes it easier to discover cross-browser incompatibility bugs. However, if no reset is performed at the end of each test or when teardown occurs, your test itself is more prone to bugs.
The tested system does not directly call the global function math. Random to determine the initial image status of each axis. On the contrary, the tiger machine depends on the parameters provided to it during creation to obtain these numbers. This allows us to test a piece of uncertain code, as if it were completely predictable. Note that the test does not cover the native math. Random Implementation of the browser, thus avoiding the risk and side effects of state changes.
Wait a moment... does the test function have more than one assertion? Multiple people in the agile community think that more than one asserted in each test is evil. However, the actual applications used to make money seldom write Test suites like this. Many people are surprised to see how many assertions each test in the physical testing suite of JUnit framework itself has.
The object's constructor and render methods look like this:
/** * Constructor for the slot machine. */drw.SlotMachine = function(buttonElement, balanceElement, reels, random, networkClient) { this.buttonElement = buttonElement; this.balanceElement = balanceElement; this.reels = reels; this.random = random; this.networkClient = networkClient; this.balance = 0;};drw.SlotMachine.prototype.render = function() { this.buttonElement.disabled = true; this.buttonElement.value = 'Pay to play'; this.balanceElement.innerHTML = 0; for(var i = 0; i < this.reels.length;){ this.reels[i++].src = 'images/' + this.random() + '.jpg'; }};
Let's put some money into the tiger machine. In this scenario, the tiger machine asynchronously calls the server to return the user balance. This is challenging because unit tests do not contain networks and Ajax calls fail. When writing unit tests, we should try to write code without any side effects. Io also belongs to this category.
function testGetBalanceGoesToNetwork(){ var url, callback; var networkStub = { send : function() { url = arguments[0]; callback = arguments[1]; } }; var slotMachine = new drw.SlotMachine(null, null, null, null, networkStub); slotMachine.getBalance(); assertEquals('/getBalance.jsp', url); assertEquals('function', typeof callback);}
This test uses network stub. What is stub? What is the difference between stub and mock? Many developers often confuse these two words and think they are synonyms. In the testing community, Stub is reserved for status-based testing. In JavaScript, it usually refers to a simple object literal that can return a pre-hardcoded value. The word mock is reserved for interactive testing. Mock can be used for behavior training. These actions interact with the tested object and can be verified.
Through the network client stub, we can now test the getbalance method. Through the local variable URL and callback, the object literal stub applied to the constructor can record its interaction with the tested system. These local variables allow us to execute assertions at the end of the test. Unfortunately, we have used the wrong tool. This is a classic example to illustrate the limitations of stub and why the mock object is used. The purpose of this test is not to verify the behavior of the tested system after a certain State is given. The test focuses on the interaction between the drw. slotmachine instance and one of its collaborators, the network client.
Jsmock, JavaScript mock Object Library
You will find that testgetbalancegoestonetwork has created its own mini mocking framework. Now Let's rebuild the test and use a general mocking framework. We need to add an independent script tag on the test page and rewrite the test like this:
<script type='text/javascript' src='../jsmock/jsmock.js'></script> function testGetBalanceWithMocks(){ var mockControl = new MockControl(); var networkMock = mockControl.createMock({ send : function() {} }); networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function)); var slotMachine = new drw.SlotMachine(null, null, null, null, networkMock); slotMachine.getBalance(); mockControl.verify(); }
Now we can get the same feedback with less code, and even lay a simpler foundation for further testing. How can this be done? The first line of the Code uses the mockcontrol constructor provided by jsmock to create an object. The Code creates a mock object with the send method. In an application with the actual networkclient class, we do not even need to apply an object literal to the createmock method. Jsmock can be inferred from the prototype.
var mock = mockControl.createMock(NetworkClient.prototype);
Once the mock object of the network client is created, the send method with specific parameters is expected to be called once. We are concerned that the server resource name is correct, and the second parameter is a callback function. The mock object is injected into the constructor of the tested system. The interaction behavior is verified through the mockcontrol object verification method to draw a conclusion. If, for any reason, the implementation of the tiger machine does not call the send method of the network client or is inconsistent with the expected parameters, the verification method throws an exception and the test fails.
Now let's write another test to verify when and how often a drw. slogmachine instance returns to the client. If the getbalance method is called before the server responds, we do not want the balance to be returned twice. This will cause the balance of the tiger machine to be returned to the user account twice, and the extra bandwidth will be spent.
function testGetBalanceWithMocksToTheNetworkOnce(){ var mockControl = new MockControl(); var networkMock = mockControl.createMock({ send : function() {} }); networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function)); var slotMachine = new drw.SlotMachine(null, null, null, null, networkMock); slotMachine.getBalance(); slotMachine.getBalance(); // no response from server yet slotMachine.getBalance(); // still no response mockControl.verify(); }
Remember our first crack here? At that time, we created our own mini mocking framework? It looks like a practical solution, but you can imagine how much code will be written to test such interactions. For the sake of parameters, let's look at the flaws in a pure stub solution.
function testGetBalanceFlawed(){ var networkStub = { send : function() { if(this.called) throw new Error('This should not be called > 1 time'); this.called = true; } }; var slotMachine = new drw.SlotMachine(null, null, null, null, networkStub); slotMachine.getBalance(); slotMachine.getBalance(); // no response from server yet slotMachine.getBalance(); // still no response }
The test asserted that the network client was called only once. After the first use, the network stub simply throws an error. There is a small problem here, because the test is a manual control of the object to be tested assertion. For example, if the system to be tested calls the network stub's send function multiple times and it handles the thrown exception itself, the test will never fail, because test runner will never receive any notifications of problems. One solution is to create a more refined mini mocking framework, but general methods such as jsmock are usually simpler.
Jsmock not only allows us to test the call sequence and parameter values of methods. This test demonstrates the behavior of the tiger machine when the network fails.
function testGetBalanceWithFailure(){ var buttonStub = {}; var mockControl = new MockControl(); var networkMock = mockControl.createMock({ send : function() {} }); networkMock.expects() .send('/getBalance.jsp', TypeOf.isA(Function)) .andThrow('network failure'); var slotMachine = new drw.SlotMachine(buttonStub, null, null, null, networkMock); slotMachine.getBalance(); assertEquals('Sorry, can't talk to the server right now', buttonStub.value); mockControl.verify(); }
Here we verify that the tiger machine can gracefully fail when the network fails. This is a good example of unit testing being better than system integration testing. Can you imagine the time and money it takes to manually simulate a network fault for each integration point on the server during each QA/release cycle?
The implementation of the getbalance method now looks like this:
drw.SlotMachine.prototype.getBalance = function() { if(this.balanceRequested) return; try{ // this line of code requires the very excellent functional.js // library, found at http://osteele.com/sources/javascript/functional this.networkClient.send('/getBalance.jsp', this.deposit.bind(this)); this.balanceRequested = true; }catch(e){ this.buttonElement.value = 'Sorry, can't talk to the server right now'; }};
Compared with stub, one disadvantage of mock is that it is quite coupled with the tested system, at least for use. When the behavior of the tested system does not match the expectation, you want the test to fail-if you do not want any changes to the encapsulated implementation details, the test will fail. To make up for this situation, jsmock provides the ability to relax expectations. You have seen this example. When we prepare a network mock object, we write the following:
networkMock.expects().send('/getBalance.jsp', TypeOf.isA(Function));
We didn't specify which callback function will be used as the second parameter. It only needs to be a callback function. If we want to extend these expectations, we can try like this:
networkMock.expects().send(TypeOf.isA(String), TypeOf.isA(Function));
If we want to reference the actual callback function of the send method of the network client mock, we can use the andstub method of the jsmock framework:
var depositCallback;networkMock.expects() .send('/getBalance.jsp', TypeOf.isA(Function)) .andStub( function(){depositCallback = arguments[1];} );depositCallback({responseText:"10"});
Before we proceed, we need to know about the mock object. Notice how the mockcontrol verify method is called at the end of each test. This is important. The unit test will not fail if the verify method is not called. Many developers have encountered such a problem. After writing some standard unit test functions, they think it is better to move the verify method from the test function to the teardown function. Although this saves several lines of code, you do not have to remember this important detail at the end of each function test. Unfortunately, it will bring you a new problem: the exception thrown in teardown will be overwritten by the first exception thrown in the test. The second trap is that new users often overuse mock objects and use them to completely replace stub. Do not. Use stub for status-based testing and mock for interaction-based testing.
One win scenario test
We can test the following scenarios with any knowledge we have learned. This test simulates a situation where a user loses first on a tiger machine and then wins.
function testLoseThenWin(){ var buttonStub = {}; var balanceStub = {}; var reelsStub = [{},{},{}]; // a losing combination, followed by a winning combination var randomNumbers = [2, 1, 3].concat([4, 4, 4]); var randomStub = function(){return randomNumbers.shift();}; var slotMachine = new drw.SlotMachine(buttonStub, balanceStub, reelsStub, randomStub); var balance = 10; slotMachine.deposit({responseText: String(balance)}); slotMachine.play(); assertEquals(balance - 1, balanceStub.innerHTML); assertEquals('Sorry, try again', buttonStub.value); slotMachine.play(); assertEquals('balance - 2 + 40', 48, balanceStub.innerHTML); assertEquals('You Won!', buttonStub.value); assertEquals('images/4.jpg', reelsStub[0].src); assertEquals('images/4.jpg', reelsStub[1].src); assertEquals('images/4.jpg', reelsStub[2].src);}
The play method of the drw. slotmachine class is implemented as follows:
drw.SlotMachine.prototype.play = function(){ var outcomes = []; var msg = 'Sorry, try again'; for(var i = 0; i < this.reels.length; i++){ this.reels[i].src = 'images/' + (outcomes[i] = this.random()) + '.jpg'; } if(outcomes[0] == outcomes[1] && outcomes[0] == outcomes[2]){ msg = 'You Won!'; this.balance += (outcomes[0] * 10); } this.buttonElement.value = msg; this.balanceElement.innerHTML = --this.balance;};
Finally, this is an example of a running tiger machine:
References
- Jsmock is a fully functional mock object library for Javascript. The author is Justin dewind.
- Jsunit, a unit testing framework for client (in the browser) JavaScript
- Mocks aren't stubs, An article by Martin Fowler
- Functional is a functional program class library for Javascript, written by Oliver Steele
- Dependency Injection, An article by Martin Fowler
Author Profile
Dennis Byrne lives in Chicago and works at drw trading, a proprietary trading firm and market maker ). He is a writer and speaker and an active member of the open source community.
View Original English text: Javascript test driven development with jsunit and jsmock.
Address: http://www.infoq.com/cn/articles/javascript-tdd