[英] 如何编写自动化、跨浏览器的 Javascript 单元测试?

1,405 阅读9分钟
原文链接: philipwalton.com
本文已经翻译成中文《[译]如何搭建自动化、跨浏览器的 JavaScript 单元测试》,欢迎参加「掘金翻译计划」,翻译优质的技术文章。

We all know how important it is to test our code in multiple browsers. And I think for the most part, we in the web development community do a pretty good job at this—at least when first releasing a project.

What we don’t do a good job of is testing our code every time we make a change.

I know I’m personally guilty of this. I’ve had “learn how to set up automated, cross-browser JavaScript unit testing” on my to-do list for years, but every time I sat down to really figure it out, I gave up. While I’m sure this was partially due to my laziness, I think it was also due to the surprising lack of good information available on this topic.

There are a lot of tools and frameworks out there (like Karma) that claim to “make automated, JavaScript testing easy”, but in my experience these tools introduce more complexity than they get rid of (more on this later). In my experience, tools that “just work” can be nice once you’re an expert, but they’re terrible for learning. And what I wanted was to actually understand how this process worked under the hood, so that when it broke (which it always eventually does), I could fix it.

For me, the best way to fully understand how something works is to try to recreate it from scratch myself. So I decided to build my own testing tool, and then share what I learned with the community.

I’m writing this article because it’s the article I wish existed years ago when I first started releasing open source projects. If you’ve never set up automated, cross-browser JavaScript unit testing yourself but have always wanted to learn, then this article is for you. It will explain how the process works and show you how to do it yourself.

The manual testing process

Before I explain the automated process, I think it’s important to make sure we’re all on the same page about how the manual process works.

After all, automation is about using machines to off-load the repetitive parts of an existing workflow. If you try to start with automation before fully understanding the manual process, it’s unlikely you’ll understand the automated process either.

In the manual process, you write your tests in a test file, and it probably looks something like this:

var assert = require('assert');
var SomeClass = require('../lib/some-class');

describe('SomeClass', function() {
  describe('someMethod', function() {
    it('accepts thing A and transforms it into thing B', function() {
      var sc = new SomeClass();
      assert.equal(sc.someMethod('A'), 'B');
    });
  });
});

This example uses Mocha and the Node.js assert module, but it doesn’t really matter what testing or assertion library you use, it could be anything.

Since Mocha runs in Node.js, you can run this test from your terminal with the following command:

mocha test/some-class-test.js

To run this test in your browser, you’ll need an HTML file with a

mocha.setup('bdd'); window.onload = function() { mocha.run(); };

If you’re not using Node.js, then your starting point likely already looks like this HTML file, the only difference is your dependencies are probably listed individually as mocha.setup('bdd'); window.onload = function() { mocha.run(); };

To something like this:


mocha.setup('bdd');
window.onload = function() {
  var runner = mocha.run();
  var failedTests = [];

  runner.on('end', function() {
    window.mochaResults = runner.stats;
    window.mochaResults.reports = failedTests;
  });

  runner.on('fail', logFailure);

  function logFailure(test, err){
    var flattenTitles = function(test){
      var titles = [];
      while (test.parent.title){
        titles.push(test.parent.title);
        test = test.parent;
      }
      return titles.reverse();
    };

    failedTests.push({
      name: test.title,
      result: false,
      message: err.message,
      stack: err.stack,
      titles: flattenTitles(test)
    });
  };
};

The only difference between the above code and the default Mocha boilerplate is this logic assigns the results of the tests to a variable called window.mochaResults in a format that Sauce Labs is expecting. And since this new code doesn’t interfere with running the tests manually in your browser, you may as well just start using it as the default Mocha boilerplate.

To re-emphasize a point I made earlier, when Sauce Labs “runs” your tests, it’s not actually running anything, it’s simply visiting a web page and waiting until a value is found on the window.mochaResults object. Then it records those results.

Determining whether your tests passed or failed

The Start JS Unit Tests method tells Sauce Labs to queue running your tests in all the browsers/platforms you give it, but it doesn’t return the results of the tests.

All it returns is the IDs of the jobs it queued. The response will look something like this:

{
  "js tests": [
    "9b6a2d7e6c8d4fd2afeeb0ff7e54e694",
    "d38688ec7256497da6966f4523ddee76",
    "14054e68ccd344c0bed77a798a9ce1e8",
    "dbc54181f7d947458f52201ea5fcb901"
  ]
}

To determine if your tests have passed or failed, you call the Get JS Unit Test Status method, which accepts a list of job IDs and returns the current status of each job.

The idea is you call this method periodically until all the jobs have completed.

request({
  url: `https://saucelabs.com/rest/v1/${username}/js-tests/status`,
  method: 'POST',
  auth: {
    username: process.env.SAUCE_USERNAME,
    password: process.env.SAUCE_ACCESS_KEY
  },
  json: true,
  body: jsTests, 

}, (err, response) => {
  if (err) {
    console.error(err);
  } else {
    console.log(response.body);
  }
});

The response will look something like this:

{
  "completed": false,
  "js tests": [
    {
      "url": "https://saucelabs.com/jobs/75ac4cadb85e415fae957f7811d778b8",
      "platform": [
        "Windows 10",
        "chrome",
        "latest"
      ],
      "result": {
        "passes": 29,
        "tests": 30,
        "end": {},
        "suites": 7,
        "reports": [],
        "start": {},
        "duration": 97,
        "failures": 0,
        "pending": 1
      },
      "id": "1f74a237d5ba4a47b5a42570ae1e7999",
      "job_id": "75ac4cadb85e415fae957f7811d778b8"
    },
    
  ]
}

Once the response.body.complete property above is true, your tests have finished running, and you can loop through each job to report passes and failures.

Accessing tests on localhost

I’ve explained that Sauce Labs “runs” your tests by visiting a URL. Of course, that means the URL you use must be publicly available on the internet.

This is a problem if you’re serving your tests from localhost.

There are a number of solutions to this problem, including Sauce Connect (the officially recommended one), which is a proxy server created by Sauce Labs that opens a secure connection between a Sauce Labs virtual machine and your local host.

Sauce Connect is designed with security in mind, and it makes it virtually impossible for an outsider to gain access to your code. The downside of Sauce Connect is it’s quite complicated to set up and use.

If security of your code is a concern, it’s probably worth figuring out Sauce Connect; if not, there are several similar solutions that make this process a lot easier.

My solution of choice is ngrok.

ngrok

ngrok is a tool for creating secure tunnels to localhost. It gives you a public URL[2] to a web server running on your local machine, which is exactly what you need to run tests on Sauce Labs.

If you do any development or manual testing on a VM, you’ve probably already heard of ngrok, and if you haven’t, you should definitely check it out. It’s an extremely useful tool.

Installing ngrok on your development machine is as simple as downloading the binary and adding it to your path; though, if you’re going to be using ngrok in Node, you may as well install it via npm.

npm install ngrok

You can programmatically start an ngrok process from Node with the following code (see the documentation for the complete API details):

const ngrok = require('ngrok');

ngrok.connect(port, (err, url) => {
  if (err) {
    console.error(err);
  } else {
    console.log(`Tests now accessible at: ${url}`);
  }
});

Once you have a public URL to your test file, using Sauce Labs to cross-browser test your local code becomes substantially easier!

Putting all the pieces together

This article has covered a lot of topics, which might give the impression that automated, cross-browser JavaScript unit testing is complicated. But this is not the case.

I’ve framed the article from my point of view—as I was attempting to solve this problem for myself. And, looking back on my experience, the only real complications were due to the lack of good information out there as to how the whole process works and how all the pieces fit together.

Once you understand all the steps, it’s quite simple. Here they are, summarized:

The initial, manual process:

  1. Write your tests and create a single HTML page to run them.
  2. Run the tests locally in one or two browsers to make sure they work.

Adding automation to the process:

  1. Create an open-source Sauce Labs account and get a username and access key.
  2. Update your test page’s source code so Sauce Labs can read the results of the tests through a global JavaScript variable.
  3. Use ngrok to create a secure tunnel to your local test page, so it’s accessible publicly on the internet.
  4. Call the Start JS Unit Tests API method with the list of browsers/platforms you want to test.
  5. Call the Get JS Unit Test Status method periodically until all jobs are finished.
  6. Report the results.

Making it even easier

I know at the beginning of this article I talked a lot about how you didn’t need a framework to do automated, cross-browser JavaScript unit testing, and I still believe that. However, even though the steps above are simple, you probably don’t want to have to hand code them every time for every project.

I had a lot of older projects I wanted to add automated testing to, so for me it made sense to package this logic into its own module.

I do recommend you take a stab at implementing this yourself, so you can fully appreciate how it works, but if you don’t have time and you want to get testing set up quickly, I recommend trying out the library I created called Easy Sauce.

Easy Sauce

Easy Sauce is a Node package and command line tool (easy-sauce), and it’s what I now use for every JavaScript project I want to cross-browser test on the Sauce Labs cloud.

The easy-sauce command takes a path to your HTML test file (defaulting to /test/), a port to start a local server on (defaulting to 1337), and a list of browsers/platforms to test against. easy-sauce will then run your tests on Sauce Lab’s selenium cloud, log the results to the console, and exit with the appropriate status code indicating whether or not the tests passed.

To make it even more convenient for npm packages, easy-sauce will by default look for configuration options in package.json, so you don’t have to separately store them. This has the added benefit of clearly communicating to users of your package exactly what browsers/platforms you support.

For complete easy-sauce usage instructions, check out the documentation on Github.

Finally, I want to stress that I built this project specifically to solve my use-case. While I think the project will likely be quite useful to many other developers, I have no plans to turn it into a full-featured testing solution.

The whole point of easy-sauce was to fill a complexity gap that was keeping me—and I believe many other developers—from properly testing their software in the environments they claimed to support.

Wrapping up

At the beginning of this article I wrote down a list of requirements, and with the help of Easy Sauce, I can now meet these requirements for any project I’m working on.

If you don’t already have automated, cross-browser JavaScript unit testing set up for your projects, I’d encourage you to give Easy Sauce a try. Even if you don’t want to use Easy Sauce, you should at least now have the knowledge needed to roll your own solution or better understand the existing tools.

Happy testing!