2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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:

  • Twitter - @cjno
  • Christian's Blog
  • Christian's Book - Test-Driven JavaScript Development

转载于:https://my.oschina.net/uniquejava/blog/540990

我为什么要写Sinon.JS相关推荐

  1. sinon.js基础使用教程---单元测试

    原文地址:www.sitepoint.com/sinon-tutor- 译文 当我们写单元测试时一个最大的绊脚石是当你面对的代码过于复杂. 在真实的项目中,我们的代码经常要做各种导致我们测试很难进行的 ...

  2. Sinon.JS Sinon.JS

    Sinon.JS Standalone test spies, stubs and mocks for JavaScript. No dependencies, works with any unit ...

  3. js node 打包mac应用_混搭 TypeScript + GraphQL + DI + Decorator 风格写 Node.js 应用

    阅读本文的知识前提:熟悉 TypeScript + GraphQL + Node.js + Decorator + Dependency Inject 等概念.前言 恰逢最近需要编写一个简单的后端 N ...

  4. 紧跟月影大佬的步伐,一起来学习如何写好JS(下)

    如何写好js - 写代码应该关注什么

  5. 紧跟月影大佬的步伐,一起来学习如何写好JS(上)

    如何写好JS - 三大原则

  6. [html] 说说js代码写到html里还是单独写到js文件里哪个好?为什么?

    [html] 说说js代码写到html里还是单独写到js文件里哪个好?为什么? js和html还是分开比较好,一是各功能独立,界面比较干净,二是方便管理,关系清晰,三是方便引用,一些公共js独立导入可 ...

  7. sinon.stub_JavaScript测试工具对决:Sinon.js vs testdouble.js

    sinon.stub 在对真实代码进行单元测试时,有许多情况使测试难以编写. 您如何检查是否调用了函数? 您如何测试Ajax呼叫? 还是使用setTimeout编码? 就是在这种情况下,您使用测试倍数 ...

  8. sinon.js的spy、stub和mock

    sinon 做测试的知道,在 Java 的单元测试中,不能获取实际对象时,我们可以使用 Mock/Stub 对我们的代码进行mock 等操作,更好的方便我们测试. 像 EasyMock.JMock.M ...

  9. Unit Testing with Sinon.JS

    Preface Which kind of method is the easiest to test? In my opinion, the answer is like this: 哪种方法最易于 ...

最新文章

  1. python dataframe显示网格_python dataframe 输出结果整行显示的方法
  2. 边缘计算技术发展与对策研究
  3. 【漫天烟花】绚烂烟花点亮夜空也太美了叭、某程序员携带烟花秀给大家拜年啦~
  4. openstack-Icehouse版本部署安装
  5. u-boot2013.10引导linux3.10.30记录
  6. linux 系统arp检测工具,linux网络常用诊断工具
  7. 样式图片_中式门窗花格图片大全样式全面选择多
  8. python随机化序列与设置随机种子
  9. 推荐一些C#相关的网站、资源和书籍
  10. 布局管理器 4----- 相对布局
  11. Raki的读paper小记:Audio Captioning with Composition of Acoustic and Semantic Information
  12. spring扩展点五:factoryBean的使用
  13. Ferret 经度范围划定时的方向问题
  14. 【UDEV】 网卡MAC地址自动设置
  15. user_agent浏览器头部
  16. iOS 制作圆形头像图片
  17. 6.5 发散思维能力
  18. 杀戮空间2服务器协议,杀戮空间2 云服务器搭建
  19. Android 多厂商推送集成
  20. 刀片服务器的机箱显示器,思科 UCS 5100系列刀片服务器机箱

热门文章

  1. C语言-函数-学会方程你的数学能力会乘风破浪突飞猛进-学会函数你的编程能力将百尺竿头更进一步
  2. JSP学生奖学金系统JSP学生评奖评优系统JSP奖学金管理系统 JSP奖学金评定系统
  3. 北洋雷达UST-10LX基于ROS都安装使用测试小问题
  4. 1024程序员节?我们整点AI绘图玩玩吧,一文教你配置stable-diffusion
  5. 模型评估:评估矩阵和打分
  6. 个人计算机全都是多媒体计算机系统组成,多媒体计算机系统组成
  7. Ms Sql Server 2000 个人绿色版 5.62
  8. 【SaltStack官方版】—— returners——返回器
  9. 泼冷水!为什么说机器学习在很多方面被高估了? | 精选
  10. 利用rmf创造一个简单世界的小问题