文档章节

我为什么要写Sinon.JS

cyper
 cyper
发布于 2015/12/08 16:46
字数 2768
阅读 171
收藏 0

Good unit tests are focused, fast and isolated. Unfortunately, the code to test sometimes depends on other code that may comprise performance and/or isolation. In this article we will explore some of those cases, and how we can deal with them by planning for testability and loose coupling, and by using test fakes.

Performance

Performance is key to a healthy test suite. I'm not talking about micro optimizations here, but rather the difference between your tests completing in a (very) few seconds as opposed to a few minutes. If tests don't finish fast, you are less likely to run them, which will put your project at risk.

The biggest threats to acceptable performance in unit tests are mainly posed by heavy DOMmanipulation, long-running timers and network activity (e.g. XMLHttpRequest). We will go through these one by one, looking for ways to avoid them slowing down our tests.

Heavy DOM manipulation

Heavy DOM manipulation is usually the easiest one to deal with because many of the performance-related issues can be mitigated by proper planning. DOM manipulating code won't necessarily slow your tests to a crawl, but needless use of the document will cause the browser to render elements which in most cases is completely unnecessary.

As an example, consider the crude jQuery plugin I presented in my previous ScriptJunkie article. It loops the elements in the collection, finds their datetime or data-datetime attributes and replaces the element text with a "humanized" time representation such as "2 hours and 3 minutes ago".

When testing this plugin, there is no reason for the elements used in tests to be attached to the document. Creating and appending elements to the document will only waste time forcing the browser to render the elements. We want to read an attribute and set innerHTML, neither of which require the element to be attached to the document or to be rendered on screen.

Large DOM manipulation tasks can be broken up into smaller functions that accept an element as input and manipulates it rather than always fetching elements from the document using either id or a CSS selector. Doing so effectively sandboxes your code, making it easier to embed without assuming control over the document at large. It also helps the performance of your tests, which will run these functions many times over. Reducing the workload in each function means less work to repeat in the test case.

Long-running timers

Timers are frequently used in JavaScript. Examples include animation, polling and other delayed activities. Imagine a method that has a side-effect of animating the height of an element over 250 milliseconds. If we need four tests to verify all aspects of this method we will be waiting for a full second for this method alone. In a regular web application, there might be many of these, causing the test suite to slow to an unbearable crawl.

Can we plan our way around the timers? In some cases we can definitely allow functions to accept options to control the duration of delayed activities. However, if we don't need (or maybe even want) this flexibility anywhere else, it's just added bloat. A better approach is to short-circuit time all together in tests.

Faking time

To short-circuit time in tests I will show you a library called Sinon.JS (disclaimer: I wrote that). Sinon has a virtual clock which can be used to trigger timeouts created with setTimeout andsetInterval without actually passing time. Doing so has two benefits:

  • Tests that use timers don't need to be asynchronous
  • Tests that use timers will be as fast as any others

The following test shows this in action with a rather silly example. It puts an element into the document (using JsTestDriver'sHTMLDoc feature), wraps it in a jQuery instance and then animates its height to 100 pixels. Rather than using setTimeout to wait for the animation to complete, the test uses the tick method of the clock property, which was injected by Sinon (throughsinon.testCase). Finally we expect the animation to have completed.

TestCase("VirtualTimeTest", sinon.testCase({ "test should animate quickly": function () { /*:DOC += */ var element = jQuery("#hey");
        element.animate({ height: "100px" }, 500); this.clock.tick(510);

        assertEquals("100px", element.css("height"));
    }
}));

Running this test confirms that, yes, the animation does indeed complete as expected, and the test completes in 3 milliseconds (in Chrome), rather than half a second.

Network activity

Perhaps the biggest threat to decent test performance is network activity. The most common source of network activity in JavaScript comes from using XMLHttpRequest. Other examples include asset loading and JSON-P.

XMLHttpRequest and tests

Live XMLHttpRequests in tests are undesirable for a number of reasons. The most obvious drawback is that it complicates the test setup itself. In addition to the client-side code you need either the back-end app, or a simplified version of it running alongside for tests to complete successfully.

The second problem is that tests will be more brittle because they are now depending on the server as well. If there is something wrong in the server-side code, the client tests might change, which is rarely what one wants in a unit test. You may also encounter problems where tests fail because of changes in test data on the server. Not to mention that maintaining the test data on the server becomes much harder because it is not visible from the test server what data the tests expect.

The clarity and documentation effect of unit tests will also suffer when running tests against a live server. Magic values will appear everywhere in assertions because the test data is not visible or otherwise obvious from inside the test.

The final problem with using live XMLHttpRequest in tests is the performance issue. Sure, your server may mostly be so fast it won't matter, but there will still be roundtrips (possibly a lot of them). One roundtrip is worse than none, especially when multiplied by the number of tests touching network code. Also, the server may have occasional hickups causing your tests to hang or worse.

Compartmentalize network access

There are a few ways to solve the network problem in unit tests. The first, as usual, is designing the application well. If XMLHttpRequest objects or jQuery.ajax calls are scattered all the way through the code-base, something is wrong.

A better approach is to design custom interfaces that solve your application's specific networking needs, and use them throughout the application. This effectively gives you looser coupling to the server and your current way of communicating with it, which has a host of benefits in itself. Luckily, it also makes testing your application a lot easier. I will cover an example of solving the testing problem through decoupling later in this article.

Fake the server

As with timers and the clock property, Sinon.JS can help us out when testing code that somehow uses XMLHttpRequest. It provides two interfaces for doing this; a stub XMLHttpRequest object and a fake server. The fake server is a high-level interface on top of the stub XMLHttpRequest object which can allow for pretty expressive tests. To use this, you don't need to change your code at all, no matter how you do your XMLHttpRequests (IE's ActiveXObject variant included).

In this example we have a blog object, which allows us to fetch and persist blog posts. It usesXMLHttpRequest to move bytes over the wire, and a simple version might look something like the following:

var blog = {
    posts: {},

    getPost: function (id, callback, errback) { if (this.posts[id]) { typeof callback == "function" && callback(posts[id]); return;
        }

        jQuery.ajax({
            url: "/posts/" + id,
            type: "get",
            dataType: "json",

            success: function (data, status, xhr) { this.posts[id] = data; typeof callback == "function" && callback(data);
            },

            errback: function (xhr, status, exception) { typeof callback == "function" && errback(status);
            }
        });
    },

    savePost: function (blogPost, callback, errback) { // ... }
};

Note that this example also codifies the "compartmentalized network access" I was talking about previously. To test that the callback is called with the JSON from the server parsed into a JavaScript object, we will use Sinon's fake server:

TestCase("BlogPostServiceTest", sinon.testCase({
    setUp: function () { this.server.respondWith( "GET", "/posts/312",
            [200, { "Content-Type": "application/json" }, '{"title":"Unit test your JavaScript","author":"Christian Johansen"' + ',"text":"..."}']);
    }, "test should call callback with parsed data from server": function () { var blogPost;

        blog.getPost(312, function (post) {
            blogPost = post;
        }); this.server.respond();

        assertEquals({
            title: "Unit test your JavaScript",
            author: "Christian Johansen",
            text: "..." }, blogPost);
    }
}));

The setUp method "configures the server" by telling it what kind of requests it should understand, as well as how they should be responded to. In this case, there is only one request the server knows how to deal with. Any GET request for /posts/312 will result in a 200 OK with a content type of application/json and a predefined JSON string as the response body. Sinon will automatically respond with a 404 for requests not mentioned in the setup.

The test then fetches a blog post through the blog object, and assigns it to a test-local variable. Because requests are asynchronous, the Sinon server cannot immediately respond to the requests. this.server.respond(); instructs the server to process any requests so far, and then we assert that our callback was called with the expected blog post object.

Using Sinon's fake server yields tests that are highly communicative. There's no question as to why we expect the specific blog post to be returned - the reason is right there in the test. The test is now also self-contained and very fast. Triple score!

Isolation

In contrast to integration tests, unit tests should ideally exercise as small parts of the application as possible. In practice, most unit tests tend towards mini integration tests because the function under test often uses other functions to compute the final answer. Most of the time, doing so is not a problem, especially not when building bottom-up. In this case, any dependencies should already be thoroughly tested and thus we should be able to trust them.

In some cases, we want to isolate a function or object from its dependencies anyway, to either rule out the possibility of them failing or because they have inconvenient side-effects. One such example is the blog.getPost method. If we are not using the fake server or something similar, this method will hit the server. Other examples of inconvenient dependencies are ones that are cumbersome to configure and use.

Decoupling interfaces

Once again, the best way to make sure you can test your code in isolation is to decouple your interfaces such that dependencies are kept to a minimum, and such that external dependencies can be controlled from the outside.

As an example, consider a widget that loads a blog post from the server and displays an excerpt. Rather than having the widget hit the server directly, we can use the blog from before. Consider the following simplified excerpt:

var blogPostWidget = {
    displayExcerpt: function (id) { this.blog.getPost(id, function (blogPost) { // Massage blog post and build DOM elements }, function (error) { // Build DOM elements, display "Found no excerpt" });
    }
};

Notice how the blog object is a property on the widget. Testing this is easy because we don't necessarily need to use the same blog object as we would use in production. For instance, we might use a very simple one:

TestCase("BlogPostWidgetTest", { "test should fetch blog post": function () { var fetchedId;

        blogPostWidget.blog = {
            getPost: function (id) {
                fetchedId = id;
            }
        };

        blogPostWidget.displayExcerpt(99);

        assertEquals(99, fetchedId);
    }
});

This test now verifies that the method in question fetches a blog post by calling this.blog.getPost. The blog object in this test is called a fake object as it is a simplified version used only in tests. More specifically, we might call the getPost function here a spy due to the way it collects information about its use. As we have already discussed two uses-cases for Sinon.JS, I will show you an example of writing the same test using Sinon's spies:

TestCase("BlogPostWidgetTest", { "test should fetch blog post": function () {
        blogPostWidget.blog = { getPost: sinon.spy(); };

        blogPostWidget.displayExcerpt(99);

        assert(blogPostWidget.blog.getPost.called);
        assert(blogPostWidget.blog.getPost.calledWith(99));
    }
});

Using sinon.spy, we were able to free the test from most of its scaffolding, improving the readability of the test while maintaining its scope.

Forcing a specific code path

A good unit test focuses on a single behavior. Sometimes we need to control the behavior of the code's dependencies to trigger the situation we want to test. Stubs are like spies, but can additionally specify behavior by calling methods on it like returns(valueToReturn).

Assume we wanted to test that the blogPostWidget correctly displayed "Found no excerpt" when the given blog post could not be found. One way to do this would be to inject a stub that calls the errback function:

TestCase("BlogPostWidgetTest", { "test should display message when post is not found": function () {
        blogPostWidget.root = document.createElement("div");
        blogPostWidget.blog = { getPost: sinon.stub().callsArg(2); };

        blogPostWidget.displayExcerpt(99); var result = div.getElementsByTagName("h2")[0]

        assertEquals("No excerpt found", result.innerHTML);
    }
});

By calling the callsArg method on the stub, the resulting function will try to call its third argument as a function when it is called. The third argument to getPost happens to be the errback, and so we expect the widget to inform the user that there was no such post. If we wanted to specify the exact type of error to occur, we could use callsArgWith:

TestCase("BlogPostWidgetTest", { "test should display message when post times out": function () {
        blogPostWidget.root = document.createElement("div");
        blogPostWidget.blog = { getPost: sinon.stub().callsArgWith(2, "timeout"); };

        blogPostWidget.displayExcerpt(99); var result = div.getElementsByTagName("h2")[0]

        assertEquals("No excerpt found", result.innerHTML);
    }
});

A word of warning on test fakes

There are two pitfalls to avoid when using spies and stubs (and mocks, which we did not cover here). The first one deals with clean-up. You can use functions like sinon.stub to stub global methods like jQuery.ajax too (which means you have yet another way of dealing with the server-in-tests problem). However, if you fake a method in one test and you want other tests to use the real implementation you need to make sure you clean up.

One way to do this clean-up is to manually remember the stubbed methods in setUp and reset them in tearDown:

TestCase("SomeTest", {
    setUp: function () { this.jqAjax = jQuery.ajax;
    },

    tearDown: function () {
        jQuery.ajax = jqAjax;
    }, "test something": function () {
        jQuery.ajax = sinon.stub(); // ... }
});

This works, but it is error-prone and cumbersome. The second pitfall to watch out for is using stubbing while TDD-ing code. Let's say we implemented the blog post widget using TDD, and the first test we wrote ensure that the widget used blog.getPost to fetch the blog post. It might look like the following:

"test should fetch blog post": function () {
    blogPostWidget.blog = { getPot: sinon.spy() };

    blogPostWidget.displayExcerpt(99);

    assert(blogPostWidget.getPot.called);
}

When the test is confirmed failing, we continue to implement it. If we don't pay attention, we will end up with a passing test and code failing in production: we misspelled "getPost", leaving out the "s". This might seem like a silly example, but even silly stuff happens. Even when programming.

Safer fakes with Sinon.JS

Sinon, as an all-round test fake library, can help you clean up, and in some cases even help you catch typos. By using sinon.testCase, as seen in the timer and XHR examples, Sinon sandboxes each test and restores any faked method created using this.spy, this.stub and othes. These work exactly like sinon.spy and friends but they are bound to a sandbox object that Sinon handles for you. Here's the jQuery.ajax example again, letting Sinon clean up:

TestCase("SomeTest", sinon.testCase({ "test something": function () { this.stub(jQuery, "ajax"); // ... }
}));

The syntax for stubbing is slightly different, but the effects are the same. When creating spies and stubs this way, Sinon will also try to help you avoid mistyping function names. Whenever you try to create spies or stubs for properties that are not already functions, Sinon will throw an error your way. Note that you can use this form to stub built-ins as well, for example to control random numbers: this.stub(Math, "random").return(0.67);

In this article we have gone over some cases where faking parts of the application is helpful for testing. We have also seen Sinon.JS in action, but with fairly simple and somewhat contrived examples. It is hard to provide good examples of unit tests without context, so in the next and final article in this series, I will walk you through TDD-ing a working jQuery plugin that uses both timers and ajax.

 

About the Author

Originally a student in informatics, mathematics, and digital signal processing, Christian Johansen has spent his professional career specializing in web and front-end development with technologies such as JavaScript, CSS, and HTML using agile practices. A frequent open source contributor, he blogs about JavaScript, Ruby, and web development at cjohansen.no. Christian works at Gitorious.org, an open source Git hosting service.

Find Christian on:

 


本文转载自:https://msdn.microsoft.com/en-us/magazine/gg649850.aspx

共有 人打赏支持
cyper

cyper

粉丝 58
博文 685
码字总数 143171
作品 0
武汉
前端工程师
私信 提问
前端单元测试初探

原文发于我的博客:https://github.com/hwen/blogS... 要不要写单测? 关于这个 cnode 上就有个很有意思的讨论 做个调查,你的 Node 应用有写单测吗? 看完这个应该会有结论?如果没有,就回...

hwencc
2017/11/11
0
0
sinon.js基础使用教程---单元测试

译文 当我们写单元测试时一个最大的绊脚石是当你面对的代码过于复杂。 在真实的项目中,我们的代码经常要做各种导致我们测试很难进行的事情。Ajax请求,timer,日期,跨浏览器特性…或者如果...

a独家记忆
06/29
0
0
12 款 JS 代码测试必备工具

每天都会产生新的代码、用户测试工具和框架。下面的列表列出了可以完成各种测试需求的代码工具。你应该调查研究一下,看这些工具是否适用于你的技术栈和技术需求。 01. Jasmine Jasmine 是一...

oschina
2016/12/07
8.8K
5
我是如何利用一个周末时间成为前端工程师的

2周前,我将 TravisLight 开源了,这是一个建立监控的工具,也是一个用于 Travis-CI 的构件监控工具。出于兴趣,我用了一个周末的时间做了这个项目,而且我是以一个真正的前端开发者的角色来...

oschina
2012/12/25
31.2K
36
优秀Asp.Net程序员的修炼之路

初级的程序员或经验不足的程序员往往只意识到自己的程序是写给计算机的,而不会在意程序其实也是写给人的,或在意得不够、不全面。 写给机器的程序,往往追求的是运行正确、执行效率能满足要...

布雷泽
2011/03/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

《碎玉投珠》的读后感想法心得范文3800字

《碎玉投珠》的读后感想法心得范文3800字: 《碎玉投珠》是晋江作者北南2018年的作品,内容主要讲述了其17年的《两小无嫌猜》中副cp师父师叔的爱情故事。 个人并没有看过北南其他的作品,这篇...

原创小博客
26分钟前
0
0
Confluence 6 文档主题合并问答

在 Confluence 官方 前期发布的消息 中,文档主题在 Confluence 6.0 及其后续版本中已经不可用。我们知道你可能对这个有很多好好奇的问题,因此我们在这里设置了一个问答用于帮助你将这个主题...

honeymose
38分钟前
2
0
java框架学习日志-2

上篇文章(java框架学习日志-1)虽然跟着写了例子,也理解为什么这么写,但是有个疑问,为什么叫控制反转?控制的是什么?反转又是什么? 控制其实就是控制对象的创建。 反转与正转对应,正转...

白话
今天
6
0
Integer使用双等号比较会发生什么

话不多说,根据以下程序运行,打印的结果为什么不同? Integer a = 100;Integer b = 100;System.out.println(a == b);//print : trueInteger a = 200;Integer b = 200;System.out.pr...

兜兜毛毛
昨天
11
0
CockroachDB

百度云上的CockroachDB 云数据库 帮助文档 > 产品文档 > CockroachDB 云数据库 > 产品描述 开源NewSQL – CockroachDB在百度内部的应用与实践 嘉宾演讲视频及PPT回顾:http://suo.im/5bnORh ...

miaojiangmin
昨天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部