文档章节

我为什么要写Sinon.JS

cyper
 cyper
发布于 2015/12/08 16:46
字数 2768
阅读 159
收藏 0
点赞 0
评论 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
博文 618
码字总数 143161
作品 0
武汉
前端工程师
前端单元测试初探

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

hwencc ⋅ 2017/11/11 ⋅ 0

优秀Asp.Net程序员的修炼之路

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

布雷泽 ⋅ 2011/03/13 ⋅ 0

LLVM每日谈之九 谈LLVM的学习

作者:snsn1984 从接触LLVM编译器到现在,也有多半年时间了,在这多半年的时间里,也花了不少精力在上面。现在回过头来总结一下在LLVM的学习过程中的一些感悟。(注:这里对LLVM的学习,不是...

snsn1984 ⋅ 2013/02/28 ⋅ 1

我为什么要坚持写博客?

题记 2017转眼即逝,这周已经是2017的最后一周了,今天我聊聊为什么要写博客。 我从接触Android到现在,差一个月的时间就满三年了,写博客也写了两年半的时间了。从15年四月到现在。从阅读量...

⋅ 2017/12/26 ⋅ 0

为什么node的标准库这么难用?

很简单的一个httpclient操作,写起来这么复杂? 为什么不像jquery里的get方法学习? 一个简单的download方法,明明只需要url和保存的路径,为什么用nodejs的标准库要写那么一大堆东西才能实现...

五杀联盟 ⋅ 2013/10/10 ⋅ 8

android布局设置问题

布局为什么有这样的代码;怎么理解,为什么要这样写呢;还有红色字体为什么要那样写呀;为什么不直接LazySrollView呀;还有蓝色区域的作用是什么呀;谢谢;希望详细一点;非常感谢

weng4570 ⋅ 2013/11/04 ⋅ 1

那些年薪百万的程序员“咸鱼翻身”没有透露的秘密

  首先,世界上没有最好的程序语言,只有最适合个人程序语言。程序语言本来就只是工具、只是手段,从来都不是重点,重点是:你的目的是什么?你要解决的问题是什么?   假设你今天想要写...

程序员客栈 ⋅ 2016/07/28 ⋅ 1

写最少的代码,避免给自己找麻烦

软件开发的一个最基本的事实是:我们必须要写代码,但对于这样的一个事实的最大一个误解是:我们的工作就是写代码。作为软件程序员的最初几年,我一直被这样的思想所迷惑,写代码是一种很强的...

oschina ⋅ 2012/07/09 ⋅ 17

外刊IT评论:为什么你要做一名程序员?

本文是从 Why why why why why are you a developer? 这篇文章翻译而来。 做一个程序员很忙,你需要去写代码,去创建meme,去进行测试,以及随时关注最新最热的gem/开源软件技术。最近,我一...

红薯 ⋅ 2011/07/14 ⋅ 16

你应该这样写”年度总结“

胖胖所在的公司不需要写年度总结,所以我感觉很幸运。但是很多人所在的工作就需要些年度总结了。年度总结怎么写?怎么才能得到领导的赞赏,一下奉献我个人的一点建议。 其实写年度总结是一件...

z00w00 ⋅ 2014/01/28 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

linux 安装docker

通过以下命令下载安装docker wget -qO- https://get.docker.com | sh 执行以上命令后输出以下内容说明安装成功,注意红框中的内容,docker安装成功后默认只有root能使用,红框中给出的提示是...

haoyuehong ⋅ 11分钟前 ⋅ 0

482. License Key Formatting - LeetCode

Question 482. License Key Formatting Solution 思路:字符串转化为char数组,从后遍历,如果是大写字母就转化为小写字母,如果是-就忽略,如果遍历了k个字符(排除-)就追加一个-。 Java实现...

yysue ⋅ 29分钟前 ⋅ 0

聊聊spring cloud gateway的LoadBalancerClientFilter

序 本文主要研究一下spring cloud gateway的LoadBalancerClientFilter GatewayLoadBalancerClientAutoConfiguration spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/org/springfram......

go4it ⋅ 54分钟前 ⋅ 0

详解:Nginx反代实现Kibana登录认证功能

Kibana 5.5 版后,已不支持认证功能,也就是说,直接打开页面就能管理,想想都不安全,不过官方提供了 X-Pack 认证,但有时间限制。毕竟X-Pack是商业版。 下面我将操作如何使用Nginx反向代理...

问题终结者 ⋅ 今天 ⋅ 0

002、nginx配置虚拟主机

一、nginx配置虚拟主机可分为三种方式,分别为: 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站...

北岩 ⋅ 今天 ⋅ 0

shell脚本之死循环写法

最近在学习写shell脚本,在练习if while等流程控制时,突然它们的死循环写法是怎么样的?经过百度与亲测记录如下: for死循环 #! /bin/bashfor ((;;));do date sleep 1d...

hensemlee ⋅ 今天 ⋅ 0

苹果的ARKit2.0有多可怕,看了就知道

序言 ARKit主要由三部分组成: 跟踪(Tracking) 跟踪是ARKit的核心组件之一,其提供了设备在物理世界中的位置与方向信息,并对物体进行跟踪,如人脸。 2.场景理解(Scene Understanding) 场...

_小迷糊 ⋅ 今天 ⋅ 0

5.1 vim介绍 5.2 vim移动光标 5.3 ,5.4vim一般模式下移动光标,复制粘贴

vim命令 vim是vi的一个升级版;vim可以显示文字的颜色 安装vim这一个包vim-enhanced 如果不知道安装包,可以使用 命令下面命令来查看vim命令是那个包安装的。 [root@linux-128 ~]# yum prov...

Linux_老吴 ⋅ 今天 ⋅ 0

vim一般模式

vim 是什么 vim是什么 ? 在之前接触Linux,编辑网卡配置文件的时候我们用过了vi ,vim简单说就是vi的升级版,它跟vi一样是Linux系统中的一个文本编辑工具。 如果系统中没有vim ,需要安装一...

李超小牛子 ⋅ 今天 ⋅ 0

docker实战

构建企业级Docker虚拟化平台实战 重点剖析虚拟化和云计算概念; 分析Docker虚拟化的概念和原理; 从0开始实战Docker虚拟化平台; 基于Docker构建Nginx WEB服务器和CentOS虚拟机; 基于开源监...

寰宇01 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部