karma jasmine_使用Jasmine和Karma测试AngularJS(第2部分)

karma jasmine

我们的目标 ( Our Goal )

In this tutorial we will be creating and testing the user profile page for the employee directory we started building in Part 1 of this tutorial. The user profile page will show the details of each employee. Due to the recent comeback of Pokémon, thanks to Pokémon Go, our employee's have requested that their profile pages display an image of their favorite Pokémon as well. Thankfully for us, this provides us the opportunity to write tests for hitting a real API. We'll also write our own custom filter for our user profile page and test the filter as well. By the end of this tutorial you will have the ability to view user profiles that make HTTP requests to Pokéapi.

在本教程中,我们将为我们在本教程的第1部分中开始构建的员工目录创建和测试用户个人资料页面。 用户个人资料页面将显示每个员工的详细信息。 由于Pokémon最近卷土重来,由于PokémonGo,我们的员工要求他们的个人资料页也显示其最喜欢的Pokémon的图像。 值得庆幸的是,这为我们提供了编写测试真实API的机会。 我们还将为我们的用户个人资料页面编写自己的自定义过滤器,并测试该过滤器。 在本教程结束时,您将能够查看对Pokéapi发出HTTP请求的用户配置文件。

你应该知道什么 ( What You Should Know )

Like the previous tutorial, this one will be focused on testing our controllers, factories, and filters that will be used for our user profile page so my assumption is that you're comfortable working with JavaScript and AngularJS applications. We'll be continuing with the application that was created in the first part of this tutorial so if you haven't worked your way through that yet, I'd recommend completing that first or cloning this repository which is the end result of the tutorial.

像上一教程一样,本教程将集中于测试将用于我们的用户个人资料页面的控制器,工厂和过滤器,因此我认为您可以轻松使用JavaScript和AngularJS应用程序。 我们将继续本教程第一部分中创建的应用程序,因此,如果您还没有完成此工作,建议您先完成该步骤或克隆该存储库 ,这是本教程的最终结果。 。

测试角度控制器 ( Testing an Angular Controller )

In Part 1 of this tutorial, we created and tested a Users service but it isn't being used in our application just yet. Let's create a controller and view to display all of our users and write a test for this controller as well. Within the app directory of our application let's create a new components directory. Within this, create a users directory which will contain our view template, controller, and test file for our controller.

在本教程的第1部分中 ,我们创建并测试了Users服务,但目前尚未在我们的应用程序中使用它。 让我们创建一个控制器并查看以显示所有用户,并为该控制器编写一个测试。 在我们应用程序的app目录中,我们创建一个新的components目录。 在其中,创建一个users目录,其中将包含我们的视图模板,控制器和控制器的测试文件。

cd app
mkdir components && cd components
mkdir users && cd users
touch users.js users.spec.js users.html 

At this point your project structure should now look like this:

此时,您的项目结构现在应如下所示:

|-meet-irl
  |-app      |-components
        |-users
          |-users.js
          |-users.spec.js
          |-users.html
    |-services
      |-users
        |-users.js
        |-users.spec.js
    |-app.css
    |-app.js
    |-index.html
  |-karma.conf.js
  |-server.js

Our expectation for this view is that it will display all of our users that we defined in our Users service. So within the controller we're going to make a call to Users.all and set that to our controller's view-model object. From there we can use the ng-repeat directive to iterate over that list of users and display it in our view.

我们期望该视图将显示我们在“ Users服务中定义的所有Users 。 因此,在控制器内,我们将调用Users.all并将其设置为控制器的view-model对象。 从那里,我们可以使用ng-repeat指令遍历该用户列表,并将其显示在我们的视图中。

Before we test that functionality, let's first write a basic test for the existence of this controller in components/users/users.spec.js.

在测试该功能之前,让我们首先在components/users/users.spec.js编写一个基本的测试,以测试该控制器的存在。

describe('UsersController', function() {
  var $controller, UsersController;

  // Load ui.router and our components.users module which we'll create next
  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('components.users'));

  // Inject the $controller service to create instances of the controller (UsersController) we want to test
  beforeEach(inject(function(_$controller_) {
    $controller = _$controller_;
    UsersController = $controller('UsersController', {});
  }));

  // Verify our controller exists
  it('should be defined', function() {
    expect(UsersController).toBeDefined();
  });
});

First we create two variables: $controller and UsersController. $controller will be set to Angular's built-in controller service and UsersController will be set to the actual instance of our controller we will write.

首先,我们创建两个变量: $controllerUsersController$controller将设置为Angular的内置控制器服务,而UsersController将设置为我们将编写的控制器的实际实例。

After that, we use angular-mocks to specify which modules we'll need within this test file. In this case we'll need ui.router and components.users which we'll create to make this test pass. The need for ui.router will be seen shortly when we create our controller since we specify all of its state options within the same file.

之后,我们使用angular-mocks来指定此测试文件中需要的模块。 在这种情况下,我们将需要创建ui.routercomponents.users来通过测试。 创建控制器后,很快就会看到对ui.router的需求,因为我们在同一文件中指定了所有状态选项。

Then we create another beforeEach block with inject which is used to inject the AngularJS $controller service. We set _$controller_ to the $controller variable we created and then create an instance of our controller by calling $controller('UsersController', {}). The first argument is the name of the controller we want to test and the second argument is an object of the dependencies for our controller. We'll leave it empty for now since we're trying to keep this test as simple as possible.

然后,我们创建另一个beforeEach与块inject是用来注入AngularJS $controller 服务 。 我们将_$controller_设置_$controller_我们创建的$controller变量,然后通过调用$controller('UsersController', {})创建控制器的实例。 第一个参数是我们要测试的控制器的名称,第二个参数是控制器依赖关系的对象。 我们暂时将其保留为空,因为我们试图使此测试尽可能简单。

Finally, we end this file with a basic test for the existence of our controller with the expectation that it should be defined.

最后,我们以对该控制器存在的基本测试结束该文件,并期望应该对其进行定义。

The one line of code $controller = _$controller_; may seem unnecessary here when we could simply write UsersController = _$controller_('UsersController', {});. That would be completely valid in this specific case but in some of our later tests we'll need to instantiate controllers with different dependencies and that $controller variable will be needed. This will make more sense once we get to those tests.

一行代码$controller = _$controller_; 当我们可以简单地编写UsersController = _$controller_('UsersController', {});这里似乎没有必要UsersController = _$controller_('UsersController', {}); 。 在这种特定情况下,这将是完全有效的,但是在我们以后的一些测试中,我们将需要实例化具有不同依赖关系的控制器,并且将需要$controller变量。 一旦我们进行了这些测试,这将变得更加有意义。

With that test file written update your karma.conf.js file to include our new test file within the files property along with the file for our controller which we're about to define.

编写该测试文件后,更新您的karma.conf.js文件,以将我们的新测试文件包括在files属性中,以及将要定义的控制器文件。

files: [
    './node_modules/angular/angular.js',
    './node_modules/angular-ui-router/release/angular-ui-router.js',
    './node_modules/angular-mocks/angular-mocks.js',
    './app/services/users/users.js',
    './app/components/users/users.js',
    './app/app.js',
    './app/services/users/users.spec.js',
    './app/components/users/users.spec.js'
  ],

Restart Karma and you should now see a failing test stating our module components.users cannot be found. Let's create that and get this test to pass.

重新启动Karma,现在应该看到测试失败,说明我们的模块components.users 。找不到用户。 让我们创建它并通过此测试。

Open up components/users/users.js and add the following code.

打开components/users/users.js并添加以下代码。

(function() {
  'use strict';

  // Define the component and controller we loaded in our test
  angular.module('components.users', [])
  .controller('UsersController', function() {
    var vm = this;
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('users', {
        url: '/users',
        templateUrl: 'components/users/users.html',
        controller: 'UsersController as uc'
      });
  });
})();

Here we've declared our component components.users and the controller itself UsersController as we specified in our test file. In addition to this, we've also added a configuration for this file including its state, url, template, and controller. This configuration is why we included ui.router in our test file.

在这里,我们已经声明了组件components.users和控制器本身的UsersController正如我们在测试文件中指定的那样。 除此之外,我们还为该文件添加了一个配置,包括其状态,URL,模板和控制器。 此配置是为什么我们在测试文件中包含ui.router原因。

Save that file, restart Karma if it isn't already running, and you should now see a passing test for UsersController should be defined.

保存该文件,如果Karma尚未运行,请重新启动它,现在您应该看到UsersController should be definedUsersController should be defined的通过测试。

Now that we know our test is working at the most basic level we need to test the call to our service to get a list of users so we can populate our view. Open up /components/users/users.spec.js again and update it with another test.

现在我们知道我们的测试在最基本的级别上进行,我们需要测试对服务的调用以获取用户列表,以便我们填充视图。 再次打开/components/users/users.spec.js并使用另一个测试对其进行更新。

describe('UsersController', function() {
  var $controller, UsersController, UsersFactory;

  // Mock the list of users we expect to use in our controller
  var userList = [
    { id: '1', name: 'Jane', role: 'Designer', location: 'New York', twitter: 'gijane' },
    { id: '2', name: 'Bob', role: 'Developer', location: 'New York', twitter: 'billybob' },
    { id: '3', name: 'Jim', role: 'Developer', location: 'Chicago', twitter: 'jimbo' },
    { id: '4', name: 'Bill', role: 'Designer', location: 'LA', twitter: 'dabill' }
  ];

  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('components.users'));
  // Add the module for our Users service
  beforeEach(angular.mock.module('api.users'));

  beforeEach(inject(function(_$controller_, _Users_) {
    $controller = _$controller_;
    UsersFactory = _Users_;

    // Spy and force the return value when UsersFactory.all() is called
    spyOn(UsersFactory, 'all').and.callFake(function() {
      return userList;
    });

    // Add the factory as a controller dependency
    UsersController = $controller('UsersController', { Users: UsersFactory });
  }));

  it('should be defined', function() {
    expect(UsersController).toBeDefined();
  });

  // Add a new test for our expected controller behavior
  it('should initialize with a call to Users.all()', function() {
    expect(UsersFactory.all).toHaveBeenCalled();
    expect(UsersController.users).toEqual(userList);
  });
});

Starting at the top we've added another variable UsersFactory which we'll set to our injected Users service. After that, we've added an array of users which we borrowed from the Users service from Part 1 of this tutorial which can be found in /services/users/users.js. Then, we load the module api.users using angular-mocks. In the following beforeEach block we inject our service Users using the underscore wrapping convention and set it to our local UsersFactory variable.

从顶部开始,我们添加了另一个变量UsersFactoryUsersFactory其设置为注入的Users服务。 之后,我们添加了一系列用户,这些Users本教程第1部分Users服务中借用的,可以在/services/users/users.js找到。 然后,我们使用angular-mocks加载模块api.users 。 在下面的beforeEach块中,我们使用下划线包装约定注入服务Users并将其设置为我们的本地UsersFactory变量。

After that we add a spy to the all method of our factory and chain it with another one of Jasmine's built-in functions callFake. The callFake method allows us to intercept a call to that method and supply it our own return value. In this case, we're returning userList which we defined at the top of this file. Finally, we add our service as a dependency to UsersController when we call $controller. The property value Users refers to the service we'll inject into our actual controller and the value UsersFactory is a reference to the service we injected just two lines above it.

之后,我们将间谍添加到工厂的all方法中,并将其与Jasmine的另一个内置函数 callFakecallFake方法允许我们拦截对该方法的调用,并为其提供我们自己的返回值。 在这种情况下,我们将返回在此文件顶部定义的userList 。 最后,当我们调用$controller时,将服务添加为对UsersController的依赖。 属性值Users是指我们将注入到实际控制器中的服务,而值UsersFactory是对我们在其上方仅两行注入的服务的引用。

It's important to remember that our tests are testing expectations and not the actual implementation of our code. In this test file, we use Jasmine's callFake function to intercept the actual call and return a hardcoded list of users (our expectation). Our tests for that service don't belong here. It was already handled in Part 1 of this tutorial and the test for that method is located within /services/users/users.spec.js.

重要的是要记住,我们的测试只是在测试期望,而不是代码的实际实现。 在此测试文件中,我们使用Jasmine的callFake函数来拦截实际的调用并返回用户的硬编码列表(我们的期望)。 我们对该服务的测试不属于此处。 本教程的第1部分已经对其进行了处理,并且该方法的测试位于/services/users/users.spec.js

Finally, we add a test spec for our controller with a new expectation: should initialize with a call to Users.all(). The test has two expectations. The first expectation uses the spy we declared above and simply expects that a call to the all method will be made. The second expectation expects that the controller's view-model property users will be set to the list of users we defined above. Save the file so Karma shows a failing test and let's add the real code to our controller which should help clarify our test.

最后,我们为控制器添加了一个新的期望的测试规范: should initialize with a call to Users.all() 。 该测试有两个期望。 第一个期望使用上面我们声明的间谍,并且简单地期望将对all方法进行调用。 第二个期望是将控制器的view-model属性users设置为我们在上面定义的用户列表。 保存文件,以便Karma显示失败的测试,让我们将真实代码添加到我们的控制器中,这将有助于阐明我们的测试。

Open up our controller file /components/users/users.js and update it to make our failing tests pass.

打开我们的控制器文件/components/users/users.js并对其进行更新,以使失败的测试通过。

(function() {
  'use strict';

  angular.module('components.users', [])
  .controller('UsersController', function(Users) { // Add Users factory
    var vm = this;

    // Call all() and set it to users
    vm.users = Users.all();
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('users', {
        url: '/users',
        templateUrl: 'components/users/users.html',
        controller: 'UsersController as uc'
      });
  });
})();

We've added the Users service as a dependency to our controller and also initialize a call to Users.all and set the return value to vm.users. Save that file and our previously failing test should now pass.

我们已经将Users服务添加为控制器的依赖项,并且还初始化了对Users.all的调用, Users.all返回值设置为vm.users 。 保存该文件,我们以前失败的测试现在应该通过。

We've now created and tested the controller for our users and our users are waiting to be displayed in the browser. Open up our empty template /components/users/users.html and add the following code to iterate over our users in the UsersController.

现在,我们已经为我们的用户创建并测试了控制器,我们的用户正等待在浏览器中显示。 打开我们的空模板/components/users/users.html并添加以下代码以遍历UsersController的用户。

<div class="container">
  <div class="row">
    <div class="col-md-4" ng-repeat="user in uc.users">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title text-center">{{user.name}}</h3>
        </div>
        <div class="panel-body">
          <div><span class="glyphicon glyphicon-briefcase" aria-hidden="true"></span> {{user.role}}</div>
          <div><span class="glyphicon glyphicon-map-marker" aria-hidden="true"></span> {{user.location}}</div>
          <div><span class="glyphicon glyphicon-link" aria-hidden="true"></span> {{user.twitter}}</div>
        </div>
      </div>
    </div>
  </div>
</div>

There's one last step to get this working in the browser. Until now we've only been adding our files to karma.conf.js. First, add the controller and service to index.html.

要使此功能在浏览器中工作,还有最后一步。 到目前为止,我们仅将文件添加到karma.conf.js 。 首先,将控制器和服务添加到index.html

<head>
  ...
  ...

  <script src="services/users/users.js"></script>

  <script src="components/users/users.js"></script>

  <script src="app.js"></script>
</head>

Then add the modules to our application dependencies in app/app.js. While we're here, update $urlRouterProvider.otherwise('/') to default to the users state we just created.

然后将模块添加到app/app.js中的app/app.js程序依赖项中。 在这里时,将$urlRouterProvider.otherwise('/')更新$urlRouterProvider.otherwise('/')默认设置为我们刚刚创建的users状态。

(function() {
  'use strict';

  angular.module('meetIrl', [
    'ui.router',
    'api.users',
    'components.users'
  ])
  .config(function($urlRouterProvider) {
    $urlRouterProvider.otherwise('/users');
  });
})();

Run nodemon server.js, navigate to http://localhost:8080/#/users, and you should see our four users.

运行nodemon server.js ,导航到http://localhost:8080/#/users ,您应该看到我们的四个用户。

测试Angular Factory和Real API端点 ( Testing an Angular Factory and a Real API Endpoint )

In Part 1 we covered how to test an Angular factory and now we've tested our first controller which consumes that same factory. But that was a simple factory that returned a hard-coded list of users. How do we test a factory that makes an actual HTTP request to a real API? As I mentioned earlier we're going to display an avatar of each user's favorite Pokémon on their individual profile page using Pokéapi.

在第1部分中,我们介绍了如何测试Angular工厂,现在我们已经测试了使用该工厂的第一个控制器。 但这是一个简单的工厂,返回了硬编码的用户列表。 我们如何测试对真实API发出实际HTTP请求的工厂? 如前所述,我们将使用Pokéapi在每个用户的个人资料页面上显示每个用户喜欢的Pokémon的头像。

First, let's create a new directory in our services folder for our Pokemon service.

首先,让我们在我们的服务文件夹中为Pokemon服务创建一个新目录。

cd app/services
mkdir pokemon && cd pokemon
touch pokemon.js pokemon.spec.js

Within our factory we'll have one method, findByName, which makes a GET request to the /pokemon/ endpoint which you can see here. This one request will provide us everything we need to populate our user profiles with all the necessary Pokémon data.

在我们的工厂内,我们将有一个方法findByName ,它向/pokemon/端点发出GET请求,您可以在此处看到。 这项要求将为我们提供所有必要的神奇宝贝数据,以填充用户资料所需的一切。

Like we've done previously we'll first set up a basic test and a basic factory to ensure everything is working correctly. Open up /services/pokemon/pokemon.spec.js and add the following code.

像我们之前所做的一样,我们将首先设置一个基本测试和一个基本工厂,以确保一切正常。 打开/services/pokemon/pokemon.spec.js并添加以下代码。

describe('Pokemon factory', function() {
  var Pokemon;

  // Load the api.pokemon module which we'll create next
  beforeEach(angular.mock.module('api.pokemon'));

  // Inject the Pokemon service
  beforeEach(inject(function(_Pokemon_) {
    Pokemon = _Pokemon_;
  }));

  // Verify our controller exists
  it('should exist', function() {
    expect(Pokemon).toBeDefined();
  });
});

Then update karma.conf.js accordingly with our two new Pokémon files.

然后使用两个新的Pokémon文件相应地更新karma.conf.js

files: [
    './node_modules/angular/angular.js',
    './node_modules/angular-ui-router/release/angular-ui-router.js',
    './node_modules/angular-mocks/angular-mocks.js',
    './app/services/users/users.js',
    './app/services/pokemon/pokemon.js',
    './app/components/users/users.js',
    './app/app.js',
    './app/services/users/users.spec.js',
    './app/services/pokemon/pokemon.spec.js',
    './app/components/users/users.spec.js'
  ],

Restarting Karma should show a failing test. Add the following code to /services/pokemon/pokemon.js to make our test pass.

重新启动Karma应该显示测试失败。 将以下代码添加到/services/pokemon/pokemon.js以使我们的测试通过。

(function() {
  'use strict';

  // Define the component and controller we loaded in our test
  angular.module('api.pokemon', [])
  .factory('Pokemon', function() {
    var Pokemon = {};

    return Pokemon;
  });
})();

Within our test file for our Pokemon service we are going to handle two cases, or response types, from Pokéapi. The first is a GET request with a valid Pokémon name. In this scenario, we'll use the successful response to populate our user profile image with an image of their favorite Pokémon. The second will be for a request with an invalid Pokémon name. In this case, we'll set the user profile image to a placeholder image to preserve the look of the profile page. Let's handle the valid API request first. Jump back into /services/pokemon/pokemon.spec.js and update it with the following code.

在用于Pokemon服务的测试文件中,我们将处理Pokéapi的两种情况或响应类型。 第一个是带有有效神奇宝贝名称的GET请求。 在这种情况下,我们将使用成功的响应,用他们最喜欢的神奇宝贝的图像填充我们的用户个人资料图像。 第二个将是带有无效神奇宝贝名称的请求。 在这种情况下,我们会将用户个人资料图像设置为占位符图像,以保留个人资料页面的外观。 让我们先处理有效的API请求。 跳回到/services/pokemon/pokemon.spec.js并使用以下代码对其进行更新。

describe('Pokemon factory', function() {
  var Pokemon, $q, $httpBackend;

  // Add Pokeapi endpoint
  var API = 'http://pokeapi.co/api/v2/pokemon/';

  // Add mocked Pokéapi response
  var RESPONSE_SUCCESS = {
    'id': 25,
    'name': 'pikachu',
    'sprites': {
      'front_default': 'http://pokeapi.co/media/sprites/pokemon/25.png'
    },
    'types': [{
      'type': { 'name': 'electric' }
    }]
  };

  beforeEach(angular.mock.module('api.pokemon'));

  // Inject $q and $httpBackend for testing HTTP requests
  beforeEach(inject(function(_Pokemon_, _$q_, _$httpBackend_) {
    Pokemon = _Pokemon_;
    $q = _$q_;
    $httpBackend = _$httpBackend_;
  }));

  it('should exist', function() {
    expect(Pokemon).toBeDefined();
  });

  describe('findByName()', function() {
    var result;

    beforeEach(function() {
        // Initialize our local result object to an empty object before each test
      result = {};

      // Spy on our service call but allow it to continue to its implementation
      spyOn(Pokemon, "findByName").and.callThrough();
    });

    it('should return a Pokemon when called with a valid name', function() {
      var search = 'pikachu';

      // Declare the endpoint we expect our service to hit and provide it with our mocked return values
      $httpBackend.whenGET(API + search).respond(200, $q.when(RESPONSE_SUCCESS));

      expect(Pokemon.findByName).not.toHaveBeenCalled();
      expect(result).toEqual({});

      Pokemon.findByName(search)
      .then(function(res) {
        result = res;
      });

      // Flush pending HTTP requests
      $httpBackend.flush();

      expect(Pokemon.findByName).toHaveBeenCalledWith(search);
      expect(result.id).toEqual(25);
      expect(result.name).toEqual('pikachu');
      expect(result.sprites.front_default).toContain('.png');
      expect(result.types[0].type.name).toEqual('electric');
    });
  })
});

At the top of this file we've added a few more variables: $httpBackend, $q, API, and RESPONSE_SUCCESS. API simply serves as a variable for the Pokéapi endpoint we're hitting and RESPONSE_SUCCESS is one example of a successful response from Pokéapi for the resource "pikachu". If you look at the example response in the documentation or hit the endpoint yourself with Postman you'll see there is a lot of data that's returned. We'll only be using a small set of that data so we've removed everything else while maintaining the data structure of the response for these four fields.

在此文件的顶部,我们添加了更多变量: $httpBackend$qAPIRESPONSE_SUCCESSAPI只是用作我们要访问的Pokéapi端点的变量,而RESPONSE_SUCCESS是Pokéapi对资源“ pikachu”成功响应的一个示例。 如果您查看文档中的示例响应,或者自己用Postman命中端点,您将看到返回了很多数据。 我们将只使用一小部分数据,因此在保留这四个字段的响应数据结构的同时,我们删除了所有其他内容。

We then set $q and $httpBackend to their respective injected services in our second beforeEach call. The $q service allows us to simulate resolving or rejecting a promise which is important when testing asynchronous calls. The $httpBackend service allows us to verify whether or not our Pokemon factory makes an HTTP request to Pokéapi without actually hitting the endpoint itself. The two of these services combined provide us the ability to verify a request was made to the API while also giving us the option to resolve or reject the response depending on which response we are testing.

然后,在第二个beforeEach调用中,将$q$httpBackend为它们各自的注入服务。 $q服务允许我们模拟解析或拒绝诺言,这在测试异步调用时很重要。 $httpBackend服务使我们能够验证Pokemon工厂是否向Pokéapi发出HTTP请求,而无需实际访问端点本身。 这两项服务的结合使我们能够验证对API的请求,同时还可以根据我们测试的响应来选择解析还是拒绝响应。

Remember that an API and it's various responses are expectations of our application. We're merely testing that our application will be able to consume those various responses. As mentioned earlier, we'll want to set the profile image to a Pokémon if it's valid or default to a placeholder image if the request is invalid.

请记住,API及其各种响应是我们应用程序的期望 。 我们只是在测试我们的应用程序将能够使用那些各种响应。 如前所述,我们希望将个人资料图片设置为“神奇宝贝”(如果有效),或者将默认设置为占位符图片(如果请求无效)。

Below our previous test we've added another describe block for the findByName method which will make an HTTP request to the Pokeapi. We declare a variable result which will be set to the result of our service call and set it to an empty object before each test is run in our beforeEach block. We also create a spy on the findByName method and chain it with another one of Jasmine's built-in functions callThrough. By chaining the spy with callThrough we have the ability to track any calls made to this function but the implementation will continue to the HTTP request that will be made within the function itself.

在之前的测试下面,我们为findByName方法添加了另一个describe块,该块将向Pokeapi发出HTTP请求。 我们声明一个变量result ,该结果将设置为服务调用的结果,并在每次测试在beforeEach块中运行之前将其设置为空对象。 我们还将在findByName方法上创建一个间谍并将其与Jasmine的另一个内置函数 callThrough 。 通过将间谍与callThrough链接callThrough我们可以跟踪对此函数的所有调用,但是实现将继续对将在函数本身中进行的HTTP请求进行跟踪。

Finally, we have our test spec for an API call to Pokéapi service with a valid Pokemon name. After declaring our search value as "pikachu" we make our first use of the $httpBackend service we injected earlier. Here we've called the whenGET method and supplied it with the API variable we defined earlier along with our search term "pikachu". We then chain it with respond and provide it two arguments: 200 as the status code and RESPONSE_SUCCESS as its return value wrapped with $q.when. When $q.when wraps a value it converts it into a simulated resolved "then-able" promise which is the behavior we'd expect when calling an Angular service that returns a promise. So in plain English this says, "When a GET request is made to http://pokeapi.co/api/v2/pokemon/pikachu, respond with a 200 status code and the resolved response object we created earlier."

最后,我们具有使用有效的Pokemon名称对API调用Pokéapi服务的测试规范。 在将search值声明为“ pikachu”之后,我们首先使用了先前注入的$httpBackend服务。 在这里,我们调用了whenGET方法,并为其提供了我们之前定义的API变量以及搜索词“ pikachu”。 然后,我们用IT连锁respond ,并为其提供两个参数:200状态码和RESPONSE_SUCCESS作为包裹着它的返回值$q.when 。 当$q.when包装一个值时,它会将其转换为模拟的已解析“ then-able” promise,这是我们在调用返回$q.when的Angular服务时期望的行为。 因此,用通俗的英语说:“当对http://pokeapi.co/api/v2/pokemon/pikachu进行GET请求时,以200状态代码和我们之前创建的已解析响应对象进行响应。”

After this we create two expectations: one for the initial state of our result variable and another for the Pokemon service call. We're expecting our spy on findByName not to have been called and the result variable to be an empty object. Then we call Pokemon.findByName, pass in our search term and chain it with .then where we set the returned result to our local result variable. After that we call $httpBackend.flush.

此后,我们创建两个期望:一个期望结果变量的初始状态,另一个期望用于Pokemon服务调用。 我们期望未调用findByName上的间谍,并且结果变量为空对象。 然后我们调用Pokemon.findByName ,通过我们的搜索项和IT连锁与.then ,我们设定的返回结果给我们当地的result变量。 之后,我们调用$httpBackend.flush

If we were to call Pokemon.findByName in a controller, the service's $http request would respond asynchronously. Within our unit tests this asynchronous behavior would be difficult to test. Thankfully, Angular's $httpBackend service provides us the ability to "flush" pending requests so we can write our tests in a synchronous manner. Because of this, it is important that any expectations we have that would come after an asynchronous call is finished are placed after our $httpBackend.flush() call in our test.

如果我们要在控制器中调用Pokemon.findByName ,则该服务的$http请求将异步响应。 在我们的单元测试中,这种异步行为将很难测试。 幸运的是,Angular的$httpBackend服务使我们能够“刷新”未决请求,因此我们可以以同步方式编写测试。 正因为如此,重要的是我们有什么期待要跟从异步调用完成是我们的后放置很重要, $httpBackend.flush()在我们的测试呼叫。

Finally, we create our final set of expectations from the result of our service call. Our first expectation utilizes the spy we created earlier to verify our service was called with the correct search term and the remaining four expectations verify that our result object contains all of the data related to Pikachu. Save that file and you should now see Karma showing a failing test.

最后,我们根据服务电话的结果创建最终的期望值。 我们的第一个期望利用我们之前创建的间谍来验证是否使用正确的搜索词调用了我们的服务,其余四个期望则证明了我们的结果对象包含与皮卡丘有关的所有数据。 保存该文件,您现在应该看到Karma显示测试失败。

We can get this test to pass in our service with just a few small additions. Open up /services/pokemon/pokemon.js and add the findByName method.

我们可以通过少量添加使该测试通过我们的服务。 打开/services/pokemon/pokemon.js并添加findByName方法。

(function() {
  'use strict';

  angular.module('api.pokemon', [])
  .factory('Pokemon', function($http) {  // Add $http dependency
    var API = 'http://pokeapi.co/api/v2/pokemon/';
    var Pokemon = {};

    // Spy on this method chained with callThrough() allows it to continue to continue to $http.get()
    Pokemon.findByName = function(name) {
      return $http.get(API + name)
      .then(function(res) {
        return res.data;
      });
    };

    return Pokemon;
  });
})();

Before adding the findByName method itself, we've injected the $http service into our factory and also created an API variable set to the Pokéapi endpoint we want to hit similar to the way we did in our test. After that we declare the findByName method and make a GET request to the endpoint with the name provided to us when the service is called. When the promise is fulfilled, we return the response's data property. Save that change and your first test for an Angular factory hitting a real API should now be passing!

在添加findByName方法本身之前,我们已经将$http服务注入了我们的工厂,并且还创建了一个API变量集,该变量集设置为我们想要命中的Pokéapi端点,这与测试中的方法类似。 之后,我们声明findByName方法,并使用调用服务时提供给我们的名称向端点发出GET请求。 兑现承诺后,我们将返回响应的data属性。 保存该更改,您的Angular工厂首次使用真实API的测试现在应该通过了!

That test handles our first case where a request is made to Pokéapi with a valid Pokemon. But we still need to handle the case where we make a request to the API with an invalid Pokémon. Within the context of our factory, we're going to test that we're able to catch a promise rejection from the API. Go back into /services/pokemon/pokemon.spec.js and add another test spec for our findByName method.

该测试处理了我们的第一种情况,即使用有效的Pokemon向Pokéapi发出请求。 但是,我们仍然需要处理使用无效的神奇宝贝向API发出请求的情况。 在工厂的上下文中,我们将测试是否能够从API catch承诺。 返回到/services/pokemon/pokemon.spec.js并为我们的findByName方法添加另一个测试规范。

describe('Pokemon factory', function() {
  var Pokemon, $q, $httpBackend;
  var API = 'http://pokeapi.co/api/v2/pokemon/';
  var RESPONSE_SUCCESS = {
    'id': 25,
    'name': 'pikachu',
    'sprites': {
      'front_default': 'http://pokeapi.co/media/sprites/pokemon/25.png'
    },
    'types': [{
      'type': { 'name': 'electric' }
    }]
  };

  // Add new mocked Pokéapi response
  var RESPONSE_ERROR = {
    'detail': 'Not found.'
  };

  beforeEach(angular.mock.module('api.pokemon'));

  beforeEach(inject(function(_Pokemon_, _$q_, _$httpBackend_) {
    Pokemon = _Pokemon_;
    $q = _$q_;
    $httpBackend = _$httpBackend_;
  }));

  it('should exist', function() {
    expect(Pokemon).toBeDefined();
  });

  describe('findByName()', function() {
    var result;

    beforeEach(function() {
      result = {};
      spyOn(Pokemon, "findByName").and.callThrough();
    });

    it('should return a Pokemon when called with a valid name', function() {
      var search = 'pikachu';
      $httpBackend.whenGET(API + search).respond(200, $q.when(RESPONSE_SUCCESS));

      expect(Pokemon.findByName).not.toHaveBeenCalled();
      expect(result).toEqual({});

      Pokemon.findByName(search)
      .then(function(res) {
        result = res;
      });
      $httpBackend.flush();

      expect(Pokemon.findByName).toHaveBeenCalledWith(search);
      expect(result.id).toEqual(25);
      expect(result.name).toEqual('pikachu');
      expect(result.sprites.front_default).toContain('.png');
      expect(result.types[0].type.name).toEqual('electric');
    });

    it('should return a 404 when called with an invalid name', function() {
      // Update search term
      var search = 'godzilla';

      // Update status code and response object (reject instead of when/resolve)
      $httpBackend.whenGET(API + search).respond(404, $q.reject(RESPONSE_ERROR));

      expect(Pokemon.findByName).not.toHaveBeenCalled();
      expect(result).toEqual({});

      // Update chained method to catch
      Pokemon.findByName(search)
      .catch(function(res) {
        result = res;
      });
      $httpBackend.flush();

      expect(Pokemon.findByName).toHaveBeenCalledWith(search);
      expect(result.detail).toEqual('Not found.');
    });
  });
});

This new test is nearly identical to our previous test. At the top of our file we added another variable RESPONSE_ERROR which is the response we expect to receive if we pass it an invalid name. In our second test, we declare that we expect to receive a 404 when hitting the API with an invalid name. From there we change our search term from "pikachu" to "godzilla" and update our whenGET to respond with a 404 status code and our new RESPONSE_ERROR variable wrapped with q.reject so that we can catch our rejected promise when we call Pokemon.findByName. Finally, we update our expectations for our result to test for the detail property of our response.

此新测试与我们之前的测试几乎相同。 在文件的顶部,我们添加了另一个变量RESPONSE_ERROR ,这是如果我们传递一个无效名称则希望收到的响应。 在第二个测试中,我们声明希望以无效名称访问API时会收到404。 在这里,我们将搜索词从“ pikachu”更改为“ whenGET ”,并更新whenGET以响应404状态代码和包装有q.rejectRESPONSE_ERROR变量,以便我们在调用Pokemon.findByName时可以catch被拒绝的承诺。 最后,我们更新对result的期望,以测试响应的detail属性。

The Pokéapi documentation isn't explicit about this response error object but I hit the API with multiple, incorrect search terms and received the same response every time. I also looked into the project on Github and the else statement for this call raises a 404 if it can't find a match for our given search term. The 404 is more important here anyway since we'll be defaulting to a placeholder image instead of using the returned response text in our view.

Pokéapi文档未明确说明此响应错误对象,但我使用多个错误的搜索词访问了API,每次都收到相同的响应。 我还查看了Github上的项目,如果找不到与给定搜索词匹配的项目,则此调用的else语句会引发404。 无论如何,404在这里更重要,因为我们将默认使用占位符图像,而不是在视图中使用返回的响应文本。

Save that file and Karma should now show our new test as a failing test. Go back into /services/pokemon/pokemon.js and add a catch to our HTTP request.

保存该文件,Karma现在应将我们的新测试显示为失败测试。 返回到/services/pokemon/pokemon.js并将catch添加到我们的HTTP请求中。

(function() {
  'use strict';

  angular.module('api.pokemon', [])
  .factory('Pokemon', function($http) {
    var API = 'http://pokeapi.co/api/v2/pokemon/';
    var Pokemon = {};

    Pokemon.findByName = function(name) {
      return $http.get(API + name)
      .then(function(res) {
        return res.data;
      })
      .catch(function(res) {
        return res.data;
      });
    };

    return Pokemon;
  });
})();

Save that file and our failing test should now be passing. We have now created an Angular factory that hits a real API and have the associated tests for both a valid and invalid response from Pokéapi. This fully tested service gives us the confidence to move on to the next and final part of our application where we create a new component for our user profile which will make the actual request to Pokéapi using our Pokemon factory.

保存该文件,我们的失败测试现在应该通过了。 现在,我们已经创建了一个使用真实API的Angular工厂,并具有来自Pokéapi的有效和无效响应的关联测试。 这项经过全面测试的服务使我们有信心继续前进到应用程序的下一部分,这是我们为用户个人资料创建一个新组件的部分,它将使用我们的Pokemon工厂向Pokéapi发出实际请求。

对我们用户的快速更新 ( A Quick Update to Our Users )

Before we get started creating the profile page for our users, we'll need to update the users in our Users service so they each have a favorite Pokémon we can use to call our Pokemon service. What can I say? I didn't expect Pokémon to make a comeback when I was writing Part 1 of this tutorial.

在开始为我们的用户创建个人资料页面之前,我们需要更新Users服务中的Users以便他们每个人都有一个喜欢的Pokémon,我们可以使用它来调用我们的Pokemon服务。 我能说什么 在编写本教程的第1部分时,我没想到神奇宝贝会卷土重来。

Open services/users/users.js and update each user in the userList with a pokemon object and a name property within that object.

打开services/users/users.js并使用pokemon对象和该对象内的name属性更新userList每个用户。

(function() {
  'use strict';

  angular.module('api.users', [])
  .factory('Users', function() {
    var Users = {};
    var userList = [
      {
        id: '1',
        name: 'Jane',
        role: 'Designer',
        location: 'New York',
        twitter: 'gijane',
        pokemon: { name: 'blastoise' }
      },
      {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      },
      {
        id: '3',
        name: 'Jim',
        role: 'Developer',
        location: 'Chicago',
        twitter: 'jimbo',
        pokemon: { name: 'hitmonchan' }
      },
      {
        id: '4',
        name: 'Bill',
        role: 'Designer',
        location: 'LA',
        twitter: 'dabill',
        pokemon: { name: 'barney' }
      }
    ];

    ...

  });
})();

You're free to use any Pokémon here you'd like but remember to update your test files accordingly once you're done. You should make updates to /services/users/users.spec.js and /components/users/users.spec.js to reflect our new list of users and their favorite Pokémon. I've also added one invalid Pokémon for the sake of having at least one user default to our placeholder image we'll add shortly.

您可以在此处随意使用任何神奇宝贝,但请记住,一旦完成,请相应地更新测试文件。 您应该对/services/users/users.spec.js/components/users/users.spec.js进行更新,以反映我们的新用户列表及其最喜欢的神奇宝贝。 我还添加了一个无效的Pokémon,以使我们至少要添加至少一个用户默认的占位符图像。

为用户配置文件创建和测试控制器 ( Creating and Testing a Controller for User Profiles )

Before we create our user profile controller and its associated test, let's take a minute to recap the expected behavior of our profile page so we can incrementally build our way to the finished feature one test case at a time. When a user navigates to a profile page for a given user, we're going to provide the user object to our controller using the resolve property provided to us by ui-router.

在创建用户配置文件控制器及其关联的测试之前,让我们花一点时间回顾一下配置文件页面的预期行为,以便我们一次一次地逐步建立最终功能的方式。 当用户导航到给定用户的配置文件页面时,我们将使用ui-router 提供给我们resolve属性将用户对象提供给控制器。

Our first test (not including our very basic toBeDefined test) will test that our controller is instantiated with a valid resolved user. Our second test will test that a valid resolved user with a valid Pokémon will make a request to the Pokéapi using our Pokemon service. This value will eventually be set to a view-model object to be used within our view. Our third test will test that a valid resolved user with an invalid Pokémon will make a request to the Pokéapi using our Pokemon service, catch the rejection, and default our view-model object to a placeholder image. Finally, we'll add one extra test for a user that doesn't exist which will redirect to a 404 page without ever making a request to the Pokéapi. Let's get started.

我们的第一个测试(不包括我们最基本的toBeDefined测试)将测试我们的控制器已被有效的解析用户实例化。 我们的第二项测试将测试具有有效神奇宝贝的有效解析用户将使用我们的Pokemon服务向神奇宝贝发出请求。 该值最终将设置为要在我们的视图中使用的视图模型对象。 我们的第三个测试将测试有效的已解析用户和无效的Pokémon,将使用我们的Pokemon服务向Pokéapi发送请求,捕获拒绝,并将视图模型对象默认为占位符图像。 最后,我们将为不存在的用户添加一个额外的测试,该测试将重定向到404页面,而无需向Pokéapi发出请求。 让我们开始吧。

We'll start by creating the directory for our user profile controller, view, and test file as usual.

我们将像往常一样为用户配置文件控制器创建目录,查看和测试文件。

cd app/components
mkdir profile && cd profile
touch profile.js profile.spec.js profile.html

Open /components/profile/profile.spec.js and we can add our basic test for the existence of our controller.

打开/components/profile/profile.spec.js ,我们可以为控制器的存在添加基本测试。

describe('components.profile', function() {
  var $controller;

  // Load ui.router and our components.profile module which we'll create next
  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('components.profile'));

  // Inject the $controller service
  beforeEach(inject(function(_$controller_) {
    $controller = _$controller_;
  }));

  describe('ProfileController', function() {
    var ProfileController;

    beforeEach(function() {
      // Create an instance of our controller
      ProfileController = $controller('ProfileController', { });
    });

    // Verify our controller exists
    it('should be defined', function() {
      expect(ProfileController).toBeDefined();
    });
  });
});

Then add our two new files to karma.conf.js.

然后将我们的两个新文件添加到karma.conf.js

files: [
    './node_modules/angular/angular.js',
    './node_modules/angular-ui-router/release/angular-ui-router.js',
    './node_modules/angular-mocks/angular-mocks.js',
    './app/services/users/users.js',
    './app/services/pokemon/pokemon.js',
    './app/components/users/users.js',
    './app/components/profile/profile.js',
    './app/app.js',
    './app/services/users/users.spec.js',
    './app/services/pokemon/pokemon.spec.js',
    './app/components/users/users.spec.js',
    './app/components/profile/profile.spec.js'
  ],

Restarting Karma should show a failing test. Add the following to /components/profile/profile.js to make it pass.

重新启动Karma应该显示测试失败。 将以下内容添加到/components/profile/profile.js以使其通过。

(function() {
  'use strict';

  // Define the component and controller we loaded in our test
  angular.module('components.profile', [])
  .controller('ProfileController', function() {
    var vm = this;
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('profile', {
        url: '/user/:id',
        templateUrl: 'components/profile/profile.html',
        controller: 'ProfileController as pc'
      });
  });
})();

Now we're ready for our first real test to verify that our controller is instantiated with a resolved user object. Go back into /components/profile/profile.spec.js and add our new test.

现在,我们准备进行第一个实际测试,以验证是否已使用解析的用户对象实例化了控制器。 返回/components/profile/profile.spec.js并添加我们的新测试。

describe('components.profile', function() {
  var $controller;

  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('components.profile'));

  beforeEach(inject(function(_$controller_) {
    $controller = _$controller_;
  }));

  describe('ProfileController', function() {
    var ProfileController, singleUser;

    beforeEach(function() {
      // Define singleUser and add resolvedUser as a dependency to our controller 
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser });
    });

    it('should be defined', function() {
      expect(ProfileController).toBeDefined();
    });
  });

  describe('Profile Controller with a valid resolved user', function() {
    var ProfileController, singleUser;

    beforeEach(function() {
      // Mock a valid user
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };

      // Add the valid user as our resolved dependency
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser });
    });

    it('should set the view model user object to the resolvedUser', function() {
      expect(ProfileController.user).toEqual(singleUser);
    });
  });
});

Here we've added a new describe block for our tests related to a valid resolved user. This test is similar to our previous test except we've added another beforeEach call to mock our single resolved user. We then pass it in as a dependency to our controller and add an expectation that the resolved user will be set to the user view-model property in our controller.

在这里,我们为与有效的已解析用户相关的测试添加了一个新的describe块。 该测试与之前的测试类似,不同之处在于,我们添加了另一个beforeEach调用来模拟单个已解析的用户。 然后,将其作为依赖项传递给我们的控制器,并期望将解析的用户设置为控制器中的user view-model属性。

Since our tests serve as a form of documentation for our actual code, I've also added a singleUser to our previous test and passed it in as a dependency to that controller instance as well. We didn't specify any expectations about the resolved user within that test but it keeps our controller declarations consistent with our actual controller and the other controller declarations within this file. As we continue to add more dependencies in this test file, we'll go back and update our other tests to reflect this.

由于我们的测试是我们实际代码的一种文档形式,因此我还向先前的测试中添加了singleUser ,并将其作为对该控制器实例的依赖关系传入。 在该测试中,我们没有对解析的用户指定任何期望,但是它使我们的控制器声明与实际控制器以及该文件中的其他控制器声明保持一致。 当我们继续在此测试文件中添加更多依赖项时,我们将返回并更新其他测试以反映这一点。

To get this test to pass, go back into /components/profile/profile.js and update it with our new resolved property.

要使此测试通过,请返回/components/profile/profile.js并使用我们新的已解析属性对其进行更新。

(function() {
  'use strict';

  angular.module('components.profile', [])
  .controller('ProfileController', function(resolvedUser) {
    var vm = this;
    vm.user = resolvedUser;
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('profile', {
        url: '/user/:id',
        templateUrl: 'components/profile/profile.html',
        controller: 'ProfileController as pc',
        resolve: {
          // Add resolvedUser with a call to Users using $stateParams
          resolvedUser: function(Users, $stateParams) {
            return Users.findById($stateParams.id);
          }
        }
      });
  });
})();

Here we add the new resolve property to our controller configuration with resolvedUser being set to the user returned by our Users.findById method. Within the controller, we then set this to our view-model user property as we stated within our test. Once again, we're not concerned with testing expectations related to our Users service here. That's delegated to the test file for our service in /services/users/users.spec.js.

在这里,我们新添加resolve属性,以我们的控制器配置resolvedUser设定为我们返回的用户Users.findById方法。 然后,在控制器中,如我们在测试中所述,将其设置为视图模型user属性。 再一次,我们不关心与此处的“ Users服务相关的测试期望。 这被委托给/services/users/users.spec.js我们服务的测试文件。

Now that we've finished our first test for a valid resolved user, let's move on to testing a call to the Pokemon service using the resolved user's Pokemon. Go back into /components/profile/profile.spec.js and add the following updates.

现在,我们已经完成了对有效的已解析用户的首次测试,让我们继续使用已解析用户的Pokemon测试对Pokemon服务的呼叫。 返回到/components/profile/profile.spec.js并添加以下更新。

describe('components.profile', function() {
  var $controller, PokemonFactory, $q, $httpBackend;
  var API = 'http://pokeapi.co/api/v2/pokemon/';
  var RESPONSE_SUCCESS = {
    'id': 58,
    'name': 'growlithe',
    'sprites': {
      'front_default': 'http://pokeapi.co/media/sprites/pokemon/58.png'
    },
    'types': [{
      'type': { 'name': 'fire' }
    }]
  };

  // Load Pokemon service
  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('api.pokemon'));
  beforeEach(angular.mock.module('components.profile'));

  // Inject Pokemon factory, $q, and $httpBackend for testing HTTP requests
  beforeEach(inject(function(_$controller_, _Pokemon_, _$q_, _$httpBackend_) {
    $controller = _$controller_;
    PokemonFactory = _Pokemon_;
    $q = _$q_;
    $httpBackend = _$httpBackend_;
  }));

  describe('ProfileController', function() {
    var ProfileController, singleUser;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };
      // Add Pokemon dependency
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory });
    });

    it('should be defined', function() {
      expect(ProfileController).toBeDefined();
    });
  });

  // Update title to include a valid Pokémon
  describe('Profile Controller with a valid resolved user and a valid Pokémon', function() {
    var singleUser, ProfileController;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };

      // Add spy to service call
      spyOn(PokemonFactory, "findByName").and.callThrough();

      // Add PokemonFactory as a controller dependency
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory });
    });

    it('should set the view model user object to the resolvedUser', function() {
      expect(ProfileController.user).toEqual(singleUser);
    });

    it('should call Pokemon.findByName and return a Pokemon object', function() {
      // Add expectations before the request is finished
      expect(ProfileController.user.pokemon.id).toBeUndefined();
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toBeUndefined();
      expect(ProfileController.user.pokemon.type).toBeUndefined();

      // Add our HTTP request expectation and resolved response value
      $httpBackend.whenGET(API + singleUser.pokemon.name).respond(200, $q.when(RESPONSE_SUCCESS));
      $httpBackend.flush();

      // Add expectations after the request is finished
      expect(PokemonFactory.findByName).toHaveBeenCalledWith('growlithe');
      expect(ProfileController.user.pokemon.id).toEqual(58);
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toContain('.png');
      expect(ProfileController.user.pokemon.type).toEqual('fire');
    });
  });
});

Starting at the top we've added a few more variables. PokemonFactory, $q, and $httpBackend will be set to their respective injected services. API and RESPONSE_SUCCESS will be used when we test our controller's call to Pokéapi using our Pokemon service. After that we load our api.pokemon module and set all of our variables to their injected services.

从顶部开始,我们添加了更多变量。 PokemonFactory$q$httpBackend将被设置为它们各自的注入服务。 当我们使用Pokemon服务测试控制器对Pokéapi的调用时,将使用APIRESPONSE_SUCCESS 。 之后,我们加载api.pokemon模块并将所有变量设置为其注入的服务。

Then, we updated our second describe title to include our expectation that this test will be working with a valid resolved user with a valid Pokémon. In the beforeEach within this describe we add a spy to our PokemonFactory's findByName method and chain it with callThrough so that our call to the service continues on to the actual HTTP request within the service. We also add the PokemonFactory as a dependency to both of our controller instances.

然后,我们更新了第二个describe标题,以期望该测试将与具有有效神奇宝贝的有效解析用户一起使用。 在此describebeforeEach describe我们将间谍程序添加到PokemonFactoryfindByName方法中,并将其与callThrough以便对服务的调用继续对服务中的实际HTTP请求进行。 我们还将PokemonFactory添加为两个控制器实例的依赖项。

Below our previous test we then add another expectation for our controller that it will make a request using our Pokemon service. Similar to our service test, we use $httpBackend's whenGET to state the API endpoint we expect to hit and supply it with a 200 status code and our RESPONSE_SUCCESS variable we defined at the top of this file. We then flush our asynchronous request to Pokéapi and list all of our expectations for the result and the view-model properties they will be set to.

在我们之前的测试之下,然后我们为控制器添加了另一个期望,即它将使用我们的Pokemon服务发出请求。 与我们的服务测试类似,我们使用$httpBackendwhenGET声明我们希望命中的API端点,并为其提供200状态代码以及我们在此文件顶部定义的RESPONSE_SUCCESS变量。 然后,我们flush对Pokéapi的异步请求,并列出对结果的所有期望以及将它们设置为的视图模型属性。

Unlike our service test we don't actually call Pokemon.findByName here directly. Instead, that call will occur within our controller after we set our view-model's users attribute to the resolved user object as we did earlier. The expectations before that call occurs within our controller are placed above $httpBackend.flush and the expectations after that asynchronous call finishes are placed after $httpBackend.flush. This goes back to Angular's $httpBackend service providing us the ability to test asynchronous calls in a synchronous manner within our tests. As far as $httpBackend.whenGET is concerned, that can be placed anywhere within this it block and even above within our beforeEach block for this test suite. That line simply waits for a request to be made to the endpoint and responds accordingly. flush() is the magic line which triggers our service call to resolve or reject within our test case.

与我们的服务测试不同,我们实际上Pokemon.findByName在此处直接调用Pokemon.findByName 。 取而代之的是,在像前面一样将视图模型的users属性设置为已解析的用户对象之后,该调用将在控制器内发生。 之前我们的控制器内发生的呼叫的期望都放在上面$httpBackend.flush和异步调用完成后的预期之后放置$httpBackend.flush 。 这可以回溯到Angular的$httpBackend服务,该服务使我们能够在测试中以同步方式测试异步调用。 就$httpBackend.whenGET而言,它可以放置在此it块内的任何位置,甚至可以放置在此测试套件的beforeEach块内的任何位置。 该行只是等待对端点的请求并做出相应的响应。 flush()是触发我们的服务调用以在我们的测试用例中解决或拒绝的魔术线。

This may be a little confusing so let's add the code to make the test pass in our controller. Go back into /components/profile/profile.js and add our call to the Pokémon service.

这可能有点令人困惑,所以让我们添加代码以使测试通过我们的控制器。 返回/components/profile/profile.js并将我们的调用添加到Pokémon服务。

(function() {
  'use strict';

  angular.module('components.profile', [])
  .controller('ProfileController', function(resolvedUser, Pokemon) { // Add Pokemon dependency
    var vm = this;
    vm.user = resolvedUser;

    // Call our Pokemon service using our resolved user's Pokemon
    Pokemon.findByName(vm.user.pokemon.name)
    .then(function(result) {
      vm.user.pokemon.id = result.id;
      vm.user.pokemon.image = result.sprites.front_default;
      vm.user.pokemon.type = result.types[0].type.name;
    });
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('profile', {
        url: '/user/:id',
        templateUrl: 'components/profile/profile.html',
        controller: 'ProfileController as pc',
        resolve: {
          resolvedUser: function(Users, $stateParams) {
            return Users.findById($stateParams.id);
          }
        }
      });
  });
})();

First, we add the Pokemon service as a dependency to our controller. Then we call the findByName method with the resolved user's Pokémon, vm.user.pokemon.name. We then chain it with then and set all of the properties we stated earlier in our test to their respective properties in our returned result object. Before that call is made the values for id, image, and type would be undefined as we stated in our test above our call to $httpBackend.flush.

首先,我们将Pokemon服务添加为对控制器的依赖。 然后,使用解析的用户的vm.user.pokemon.name调用findByName方法。 然后,我们再将其链接起来, then将我们在测试中先前所述的所有属性设置为返回result对象中的相应属性。 在进行该调用之前,如我们在$httpBackend.flush调用上方的测试中所述, idimagetype的值将是undefined

Now that we've tested a call to the Pokemon service with a valid Pokémon, let's add the test for an invalid Pokémon. The good news is that these tests are very similar with only a few small changes.

现在,我们已经使用有效的神奇宝贝测试了对Pokemon服务的调用,现在让我们为无效的神奇宝贝添加测试。 好消息是,这些测试非常相似,只是有一些小的变化。

describe('components.profile', function() {
  var $controller, PokemonFactory, $q, $httpBackend;
  var API = 'http://pokeapi.co/api/v2/pokemon/';
  var RESPONSE_SUCCESS = {
    'id': 58,
    'name': 'growlithe',
    'sprites': {
      'front_default': 'http://pokeapi.co/media/sprites/pokemon/58.png'
    },
    'types': [{
      'type': { 'name': 'fire' }
    }]
  };

  // Add mocked Pokéapi response
  var RESPONSE_ERROR = {
    'detail': 'Not found.'
  };

  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('api.pokemon'));
  beforeEach(angular.mock.module('components.profile'));

  beforeEach(inject(function(_$controller_, _Pokemon_, _$q_, _$httpBackend_) {
    $controller = _$controller_;
    PokemonFactory = _Pokemon_;
    $q = _$q_;
    $httpBackend = _$httpBackend_;
  }));

  describe('ProfileController', function() {
    var ProfileController, singleUser;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory });
    });

    it('should be defined', function() {
      expect(ProfileController).toBeDefined();
    });
  });

  describe('Profile Controller with a valid resolved user and a valid Pokemon', function() {
    var singleUser, ProfileController;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };

      spyOn(PokemonFactory, "findByName").and.callThrough();

      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory });
    });

    it('should set the view model user object to the resolvedUser', function() {
      expect(ProfileController.user).toEqual(singleUser);
    });

    it('should call Pokemon.findByName and return a Pokemon object', function() {
      expect(ProfileController.user.pokemon.id).toBeUndefined();
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toBeUndefined();
      expect(ProfileController.user.pokemon.type).toBeUndefined();

      $httpBackend.whenGET(API + singleUser.pokemon.name).respond(200, $q.when(RESPONSE_SUCCESS));
      $httpBackend.flush();

      expect(PokemonFactory.findByName).toHaveBeenCalledWith('growlithe');
      expect(ProfileController.user.pokemon.id).toEqual(58);
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toContain('.png');
      expect(ProfileController.user.pokemon.type).toEqual('fire');
    });
  });

  // Add our new test
  describe('Profile Controller with a valid resolved user and an invalid Pokemon', function () {
    var singleUser, ProfileController;

    beforeEach(function() {
      // Update Pokémon name
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'godzilla' }
      };

      spyOn(PokemonFactory, "findByName").and.callThrough();

      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory });
    });

    it('should call Pokemon.findByName and default to a placeholder image', function() {
      expect(ProfileController.user.pokemon.image).toBeUndefined();

      // Declare the endpoint we expect our service to hit and provide it with our mocked return values
      $httpBackend.whenGET(API + singleUser.pokemon.name).respond(404, $q.reject(RESPONSE_ERROR));
      $httpBackend.flush();

      // Add expectation that our image will be set to a placeholder image
      expect(PokemonFactory.findByName).toHaveBeenCalledWith('godzilla');
      expect(ProfileController.user.pokemon.image).toEqual('http://i.imgur.com/HddtBOT.png');
    });
  });
});

First, we add another describe block for a valid resolved user with an invalid Pokémon. Then we change our pokemon value from growlithe to godzilla. From there we change our whenGET to respond with a 404 and to reject our RESPONSE_ERROR object so that we can catch it in our controller. Finally, we update our expectations for the image property. Before the promise is rejected we expect the property to be undefined. Once the promise is actually rejected, we'll set the image property to our placeholder image.

首先,我们为带有无效神奇宝贝的有效解析用户添加另一个describe块。 然后,我们从改变我们的口袋妖怪值growlithegodzilla 。 从那里,我们更改whenGET以404响应并拒绝RESPONSE_ERROR对象,以便我们可以在控制器中catch它。 最后,我们更新了对image属性的期望。 在承诺被拒绝之前,我们希望该属性未定义。 一旦承诺被实际拒绝,我们将image属性设置为占位符图像。

Earlier in the tutorial I mentioned a seemingly unnecessary line of code: $controller = _$controller_;. This is where that's paying off. When we only had one userList to test in our UsersController we could have avoided that variable declaration. But as we see here, we're now testing a controller with a slightly modified resolvedUser. While the dependency is the same the object itself is different. In this case it's the Pokémon's name so the ability to have separate controller instances within each test block is needed to successfully test our controller.

在本教程的前面,我提到了看似不必要的代码行: $controller = _$controller_; 。 这就是回报所在。 当我们在UsersController只测试一个userList时,我们可以避免该变量声明。 但是正如我们在这里看到的,我们现在正在测试带有稍微修改的resolvedUser的控制器。 尽管依赖性相同,但对象本身却不同。 在这种情况下,这就是神奇宝贝的名字,因此需要能够在每个测试块中具有单独的控制器实例才能成功测试我们的控制器。

To make this test pass open /components/profile/profile.js and complete our call to Pokemon.findByName.

要进行此测试,请打开/components/profile/profile.js并完成对Pokemon.findByName的调用。

(function() {
  'use strict';

  angular.module('components.profile', [])
  .controller('ProfileController', function(resolvedUser, Pokemon) {
    var vm = this;
    vm.user = resolvedUser;

    Pokemon.findByName(vm.user.pokemon.name)
    .then(function(result) {
      vm.user.pokemon.id = result.id;
      vm.user.pokemon.image = result.sprites.front_default;
      vm.user.pokemon.type = result.types[0].type.name;
    })
    .catch(function(result) {
      // Add the default placeholder image
      vm.user.pokemon.image = 'http://i.imgur.com/HddtBOT.png';
    });
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('profile', {
        url: '/user/:id',
        templateUrl: 'components/profile/profile.html',
        controller: 'ProfileController as pc',
        resolve: {
          resolvedUser: function(Users, $stateParams) {
            return Users.findById($stateParams.id);
          }
        }
      });
  });
})();

As our test stated, when the promise is rejected we set the image property on our view-model to a placeholder image. We're almost done. Before we finish with our last test for redirecting our users to a 404 page, let's create that component first.

正如我们的测试所述,当承诺被拒绝时,我们将视图模型上的image属性设置为占位符图像。 我们快完成了。 在完成上一次将用户重定向到404页面的测试之前,让我们首先创建该组件。

创建404组件 ( Creating 404 component )

Our 404 page is going to be extremely basic. We'll test it for the sake of extra practice but it won't do much since the page will largely be an HTML page with a hardcoded image within it. For that reason, we'll work through this without all the details since a lot of this is the boilerplate we've seen across all of our previous controller and factory tests.

我们的404页面将非常基础。 为了进行更多练习,我们将对其进行测试,但是由于页面将很大程度上是其中包含硬编码图像HTML页面,因此不会做太多事情。 出于这个原因,我们将在没有所有细节的情况下进行操作,因为很多是我们在以前的所有控制器和工厂测试中都看到的样板。

We'll start off as usual creating a directory for our 404 component.

我们将照常开始为404组件创建目录。

cd app/components
mkdir missingno && cd missingno
touch missingno.js missingno.spec.js missingno.html

In /components/missingno/missingno.spec.js we'll add our basic test for our controller.

/components/missingno/missingno.spec.js我们将为控制器添加基本测试。

describe('components.missingno', function() {
  var $controller, MissingnoController;

  // Load ui.router and our components.missingno module which we'll create next
  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('components.missingno'));

  // Inject the $controller service to create instances of the controller (UsersController) we want to test
  beforeEach(inject(function(_$controller_) {
    $controller = _$controller_;
    MissingnoController = $controller('MissingnoController', {});
  }));

  // Verify our controller exists
  it('should be defined', function() {
    expect(MissingnoController).toBeDefined();
  });
});

As usual we bring in our required modules, inject the $controller service, create an instance of our controller, and create an expectation for it to be defined.

像往常一样,我们引入所需的模块,注入$controller服务,创建$controller的实例,并为要定义的期望创建期望。

Once again, let's add our files to karma.conf.js.

再一次,让我们将文件添加到karma.conf.js

files: [
    './node_modules/angular/angular.js',
    './node_modules/angular-ui-router/release/angular-ui-router.js',
    './node_modules/angular-mocks/angular-mocks.js',
    './app/services/users/users.js',
    './app/services/pokemon/pokemon.js',
    './app/components/users/users.js',
    './app/components/profile/profile.js',
    './app/components/missingno/missingno.js',
    './app/app.js',
    './app/services/users/users.spec.js',
    './app/services/pokemon/pokemon.spec.js',
    './app/components/users/users.spec.js',
    './app/components/profile/profile.spec.js',
    './app/components/missingno/missingno.spec.js'
  ],

Then we create our controller in /components/missingno/missingno.js.

然后,我们在/components/missingno/missingno.js创建控制器。

(function() {
  'use strict';

  // Define the component and controller we loaded in our test
  angular.module('components.missingno', [])
  .controller('MissingnoController', function() {
    var vm = this;
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('404', {
        url: '/404',
        templateUrl: 'components/missingno/missingno.html',
        controller: 'MissingnoController as mn'
      });
  });
})();

And populate our view in /components/missingno/missingno.html.

并在/components/missingno/missingno.html填充我们的视图。

<div class="container">
  <div class="row">
    <div class="col-md-4 col-md-offset-4 text-center">
      <div><img src="http://i.imgur.com/5pG5t.jpg" class="missingno"></div>
      <a ui-sref="users">RUN</a>
    </div>
  </div>
</div>

Before we get this working in a browser we'll also need to add our file to index.html and our module to app.js.

在将其用于浏览器之前,我们还需要将文件添加到index.html并将模块添加到app.js

<head>
  ...
  ...

  <script src="services/users/users.js"></script>

  <script src="components/users/users.js"></script>
  <!--add our missingno component-->
  <script src="components/missingno/missingno.js"></script>

  <script src="app.js"></script>
</head>
(function() {
  'use strict';

  angular.module('meetIrl', [
    'ui.router',
    'api.users',
    'components.users',
    'components.missingno' // add missingno component
  ])
  .config(function($urlRouterProvider) {
    $urlRouterProvider.otherwise('/users');
  });
})();

Open your browser to http://localhost:8080/#/404 and you should see our newly created 404 page!

打开浏览器到http://localhost:8080/#/404 ,您应该会看到我们新创建的404页面!

Now we can update our ProfileController and its test to redirect us to this page in the case of a missing user.

现在,我们可以更新ProfileController及其测试,以便在缺少用户的情况下将我们重定向到此页面。

测试状态更改为404页的丢失用户 ( Testing a State Change to a 404 Page for Missing Users )

In the case that a user navigates to a url such as http://localhost:8080/user/scotch or http://localhost:8080/user/999, assuming a user with an id "999" doesn't exist, we'll want to trigger a state change to our new 404 page.

如果用户导航到诸如http://localhost:8080/user/scotchhttp://localhost:8080/user/999 ,并假设不存在ID为“ 999”的用户,我们将要触发状态更改到新的404页面。

Let's add our test for this new expected behavior. Open /components/profile/profile.spec.js and add our new test.

让我们为这个新的预期行为添加测试。 打开/components/profile/profile.spec.js并添加我们的新测试。

describe('components.profile', function() {
  var $controller, PokemonFactory, $q, $httpBackend, $state;
  var API = 'http://pokeapi.co/api/v2/pokemon/';
  var RESPONSE_SUCCESS = {
    'id': 58,
    'name': 'growlithe',
    'sprites': {
      'front_default': 'http://pokeapi.co/media/sprites/pokemon/58.png'
    },
    'types': [{
      'type': { 'name': 'fire' }
    }]
  };

  // Add new mocked Pokéapi response
  var RESPONSE_ERROR = {
    'detail': 'Not found.'
  };

  beforeEach(angular.mock.module('ui.router'));
  beforeEach(angular.mock.module('api.pokemon'));
  beforeEach(angular.mock.module('components.profile'));

  // Inject $state service
  beforeEach(inject(function(_$controller_, _Pokemon_, _$q_, _$httpBackend_, _$state_) {
    $controller = _$controller_;
    PokemonFactory = _Pokemon_;
    $q = _$q_;
    $httpBackend = _$httpBackend_;
    $state = _$state_;
  }));

  describe('ProfileController', function() {
    var ProfileController;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };
      // Add $state dependency
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory, $state: $state });
    });

    it('should be defined', function() {
      expect(ProfileController).toBeDefined();
    });
  });

  describe('Profile Controller with a valid resolved user and a valid Pokemon', function() {
    var singleUser, ProfileController;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'growlithe' }
      };

      spyOn(PokemonFactory, "findByName").and.callThrough();

      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory, $state: $state });
    });

    it('should set the view model user object to the resolvedUser', function() {
      expect(ProfileController.user).toEqual(singleUser);
    });

    it('should call Pokemon.findByName and return a Pokemon object', function() {
      expect(ProfileController.user.pokemon.id).toBeUndefined();
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toBeUndefined();
      expect(ProfileController.user.pokemon.type).toBeUndefined();

      $httpBackend.whenGET(API + singleUser.pokemon.name).respond(200, $q.when(RESPONSE_SUCCESS));
      $httpBackend.flush();

      expect(PokemonFactory.findByName).toHaveBeenCalledWith('growlithe');
      expect(ProfileController.user.pokemon.id).toEqual(58);
      expect(ProfileController.user.pokemon.name).toEqual('growlithe');
      expect(ProfileController.user.pokemon.image).toContain('.png');
      expect(ProfileController.user.pokemon.type).toEqual('fire');
    });
  });

  describe('Profile Controller with a valid resolved user and an invalid Pokemon', function () {
    var singleUser, ProfileController;

    beforeEach(function() {
      singleUser = {
        id: '2',
        name: 'Bob',
        role: 'Developer',
        location: 'New York',
        twitter: 'billybob',
        pokemon: { name: 'godzilla' }
      };

      spyOn(PokemonFactory, "findByName").and.callThrough();

      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory, $state: $state });
    });

    it('should call Pokemon.findByName and default to a placeholder image', function() {
      expect(ProfileController.user.pokemon.image).toBeUndefined();

      $httpBackend.whenGET(API + singleUser.pokemon.name).respond(404, $q.reject(RESPONSE_ERROR));
      $httpBackend.flush();

      expect(PokemonFactory.findByName).toHaveBeenCalledWith('godzilla');
      expect(ProfileController.user.pokemon.image).toEqual('http://i.imgur.com/HddtBOT.png');
    });
  });

  describe('Profile Controller with an invalid resolved user', function() {
    var singleUser, ProfileController;

    beforeEach(function() {
      // Add spy to $state service
      spyOn($state, "go");
      spyOn(PokemonFactory, "findByName");

      // Add $state service as a dependency to our controller
      ProfileController = $controller('ProfileController', { resolvedUser: singleUser, Pokemon: PokemonFactory, $state: $state });
    });

    it('should redirect to the 404 page', function() {
      expect(ProfileController.user).toBeUndefined();
      expect(PokemonFactory.findByName).not.toHaveBeenCalled();
      expect($state.go).toHaveBeenCalledWith('404');
    });
  });
});

At the top of the file we declare a new variable $state. We then inject the $state service and set that to our $state variable. We then add another describe block for our controller test with an invalid resolved user. Within the block we declare singleUser but leave it as undefined. If you'll recall from Part 1 of this tutorial, that's exactly the return value we would expect from our Users.findById service call and we even wrote a test for that behavior in /services/users/users.spec.js.

在文件的顶部,我们声明一个新变量$state 。 然后,我们注入$state服务并将其设置为我们的$state变量。 然后,我们使用无效的已解析用户为控制器测试添加另一个describe块。 在该块中,我们声明singleUser但将其保留为undefined 。 如果您从本教程的第1部分中回想起了,那正是我们希望从Users.findById服务调用中获得的返回值,我们甚至在/services/users/users.spec.js针对该行为编写了一个测试。

We then create two spies: one for the go method of the $state service and another for the findByName method of our PokemonFactory. We then pass in both of these as dependencies to our controllers. Finally, we create our test expectation to redirect to our 404 page. First we specify our expectation that the resolvedUser is undefined and then we utilize our spies to ensure PokemonFactory.findByName isn't called and that $state.go is called to redirect us to our 404 page.

然后,我们创建两个间谍:一个用于$state服务的go方法,另一个用于我们PokemonFactoryfindByName方法。 然后,我们将这两个都作为依赖项传递给我们的控制器。 最后,我们创建测试期望以重定向到我们的404页面。 首先,我们指定我们的期望, resolvedUserundefined ,然后我们利用我们的间谍,以确保PokemonFactory.findByName不叫和$state.go叫我们重定向到我们的404页。

We can make this failing test pass with a small update to our profile controller. Go back into /components/profile/profile.js and add the following conditional for our resolvedUser along with our new $state dependency.

我们可以通过对Profile Controller进行少量更新来使失败的测试通过。 返回到/components/profile/profile.js并添加下列条件为我们的resolvedUser我们的新一起$state的依赖。

(function() {
  'use strict';

  angular.module('components.profile', [])
  .controller('ProfileController', function(resolvedUser, Pokemon, $state) {
    var vm = this;

    // Set the resolvedUser if it exists, otherwise redirect to our 404 page
    if (resolvedUser) {
      vm.user = resolvedUser;
    } else {
      return $state.go('404');
    }

    Pokemon.findByName(vm.user.pokemon.name)
    .then(function(result) {
      vm.user.pokemon.id = result.id;
      vm.user.pokemon.image = result.sprites.front_default;
      vm.user.pokemon.type = result.types[0].type.name;
    })
    .catch(function(result) {
      vm.user.pokemon.image = 'http://i.imgur.com/HddtBOT.png';
    });
  })
  .config(function($stateProvider) {
    $stateProvider
      .state('profile', {
        url: '/user/:id',
        templateUrl: 'components/profile/profile.html',
        controller: 'ProfileController as pc',
        resolve: {
          resolvedUser: function(Users, $stateParams) {
            return Users.findById($stateParams.id);
          }
        }
      });
  });
})();

Save that file and all of our tests should now be passing.

保存该文件,我们所有的测试现在都应该通过。

It's worth noting that we added a conditional statement here for resolvedUser which made our new test for the 404 page pass without breaking our previous test for should set the view model user object to the resolvedUser. This test says nothing about how this will be done, it only cares that it actually happens. Within the if statement we could nest ten more if(true) {} statements and our test would still pass. That wouldn't make much sense logically speaking but once again our tests only care that our ProfileController behaves as expected with all of our various test cases. The implementation to make them pass is up to you.

值得注意的是,我们在这里为resolvedUser添加了一条条件语句,该语句使我们对404页面的新测试通过而又没有破坏我们先前的测试, should set the view model user object to the resolvedUser 。 该测试没有说明如何完成此操作,它只关心实际发生的情况。 在if语句中,我们可以嵌套十个if(true) {}语句,并且测试仍将通过。 这将没有多大意义按理来说却又一次我们的测试只关心我们ProfileController表现为预期与我们所有的各种测试案例。 使它们通过的实现方式取决于您。

Now that our ProfileController is completed and fully tested, let's update our template so we can see this code in action. Open up /components/profile/profile.html and add the following code.

现在,我们的ProfileController已经完成并经过了充分的测试,下面我们来更新模板,以便我们可以实际看到此代码。 打开/components/profile/profile.html并添加以下代码。

<div class="container">
  <div class="row">
    <div class="col-md-4 col-md-offset-4">
      <div class="panel panel-default">
        <div class="panel-heading">
          <div class="text-center">
            <img ng-src="{{pc.user.pokemon.image}}" class="img-circle pokemon">
          </div>
          <h3 class="panel-title text-center">{{pc.user.name}}</h3>
        </div>
        <div class="panel-body text-center">
          <div><span class="glyphicon glyphicon-briefcase" aria-hidden="true"></span> {{pc.user.role}}</div>
          <div><span class="glyphicon glyphicon-map-marker" aria-hidden="true"></span> {{pc.user.location}}</div>
          <div><span class="glyphicon glyphicon-link" aria-hidden="true"></span> {{pc.user.twitter}}</div>
          <div><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> {{pc.user.pokemon.name}}</div>
          <div><span class="glyphicon glyphicon-tag" aria-hidden="true"></span> {{pc.user.pokemon.type}}</div>
        </div>
      </div>
    </div>
  </div>
</div>

And add some styling for the profile image to app.css.

并为app.css添加一些个人资料图像app.css

.pokemon {
  max-width: 75px;
  height: 75px;
  border: 1px solid white;
}

While we're at it, let's also add the ability to navigate to this page from our /components/users/users.html page by adding a ui-sref to each user's name.

在此过程中,我们还通过向每个用户的名称添加ui-sref来添加从/components/users/users.html页面导航至此页面的功能。

<div class="container">
  <div class="row">
    <div class="col-md-4" ng-repeat="user in uc.users">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title text-center"><a ui-sref="profile({id: user.id})">{{user.name}}</a></h3>
        </div>
        <div class="panel-body">
          <div><span class="glyphicon glyphicon-briefcase" aria-hidden="true"></span> {{user.role}}</div>
          <div><span class="glyphicon glyphicon-map-marker" aria-hidden="true"></span> {{user.location}}</div>
          <div><span class="glyphicon glyphicon-link" aria-hidden="true"></span> {{user.twitter}}</div>
        </div>
      </div>
    </div>
  </div>
</div>

Finally, we'll once again need to update our index.html and app.js to include our api.pokemon and components.profile modules.

最后,我们将再次需要更新index.htmlapp.js以包含api.pokemoncomponents.profile模块。

<head>
  ...
  ...

  <script src="services/users/users.js"></script>
  <!--add pokemon service-->
  <script src="services/pokemon/pokemon.js"></script>

  <script src="components/users/users.js"></script>
  <!--add profile component-->
  <script src="components/profile/profile.js"></script>
  <script src="components/missingno/missingno.js"></script>

  <script src="app.js"></script>
</head>
(function() {
  'use strict';

  angular.module('meetIrl', [
    'ui.router',
    'api.users',
    'api.pokemon',
    'components.users',
    'components.profile',
    'components.missingno'
  ])
  .config(function($urlRouterProvider) {
    $urlRouterProvider.otherwise('/users');
  });
})();

With those final changes, you should now be able to click on each user's name in our /users page and see an image for their favorite Pokémon within their profile page.

进行了这些最终更改后,您现在应该可以在我们的/users页面中单击每个用户的名称,并在其个人资料页面中查看其最喜欢的神奇宝贝的图像。

结论 ( Conclusion )

At this point we've now learned how to test a controller and a service that hits a real API. In our service tests, we utilized $httpBackend to listen to HTTP endpoints and $q to resolve or reject our expected responses from the API. We then learned how to test our controllers with all of its dependencies including our tested services. Given that our users can have valid or invalid favorite Pokémon names, we finally learned how to test the logic within our controller. We did this using multiple controller instances within our tests each with their own resolvedUser.

至此,我们现在已经了解了如何测试符合真实API的控制器和服务。 在我们的服务测试中,我们使用$httpBackend侦听HTTP端点,并使用$q解析或拒绝来自API的预期响应。 然后,我们学习了如何使用所有依赖项(包括经过测试的服务)测试控制器。 鉴于我们的用户可以使用有效或无效的喜欢的Pokémon名称,我们最终学会了如何在控制器中测试逻辑。 我们在测试中使用多个控制器实例来完成此操作,每个控制器实例都有自己的resolvedUser

奖金-测试角度过滤器 ( Bonus - Testing an Angular Filter )

The Pokéapi is very specific about the search term it expects. The value we provide must be entirely lowercase. Send a GET request with "Pikachu" and it won't work. That's fine for our service call but when we display the user's Pokémon in their profile page we'd like it to be proper case. Let's create a simple filter to capitalize the first letter of a given string so we can use it in our view template. First, let's create a directory for our filter.

神奇宝贝对期望的搜索字词非常具体。 我们提供的值必须完全小写。 使用“皮卡丘”发送GET请求,该请求将无法正常工作。 这对于我们的服务电话很好,但是当我们在用户的个人资料页面中显示用户的神奇宝贝时,我们希望它是适当的情况。 让我们创建一个简单的过滤器以大写给定字符串的首字母,以便我们可以在视图模板中使用它。 首先,让我们为过滤器创建一个目录。

cd app/ && mkdir filters && cd filters
mkdir capitalize && cd capitalize
touch capitalize.js capitalize.spec.js

Open up /filters/capitalize/capitalize.spec.js and add the following test for our filter.

打开/filters/capitalize/capitalize.spec.js并为我们的过滤器添加以下测试。

describe('Capitalize filter', function() {
  var capitalizeFilter;

  // Load our filters.capitalize module which we'll create next
  beforeEach(angular.mock.module('filters.capitalize'));

  // Inject the $filter service and create an instance of our capitalize filter
  beforeEach(inject(function(_$filter_) {
    capitalizeFilter = _$filter_('capitalize');
  }));

  it('should capitalize the first letter of a string', function() {
    expect(capitalizeFilter('blastoise')).toEqual('Blastoise');
  });
});

Similar to our other tests, we load our module filters.capitalize, inject the $filter service, and create an instance of the filter by calling it with our service name capitalize and setting it to our capitalizeFilter variable. We then create a test for our filter providing it a lowercase Pokémon name "blastoise" with the expectation that the return value will be "Blastoise".

与其他测试类似,我们加载模块filters.capitalize ,注入$filter服务,并通过使用服务名称capitalize调用过滤器并将其设置为capitalizeFilter变量来创建过滤器实例。 We then create a test for our filter providing it a lowercase Pokémon name "blastoise" with the expectation that the return value will be "Blastoise".

Once again, add these two files to karma.conf.js to reveal our failing test.

Once again, add these two files to karma.conf.js to reveal our failing test.

files: [
    './node_modules/angular/angular.js',
    './node_modules/angular-ui-router/release/angular-ui-router.js',
    './node_modules/angular-mocks/angular-mocks.js',
    './app/services/users/users.js',
    './app/services/pokemon/pokemon.js',
    './app/components/users/users.js',
    './app/components/profile/profile.js',
    './app/components/missingno/missingno.js',
    './app/filters/capitalize/capitalize.js',
    './app/app.js',
    './app/services/users/users.spec.js',
    './app/services/pokemon/pokemon.spec.js',
    './app/components/users/users.spec.js',
    './app/components/profile/profile.spec.js',
    './app/components/missingno/missingno.spec.js',
    './app/filters/capitalize/capitalize.spec.js'    
  ],

Then we can go into /filters/capitalize/capitalize.js and create our filter.

Then we can go into /filters/capitalize/capitalize.js and create our filter.

(function() {
  'use strict';

  // Define the component and filter we loaded in our test
  angular.module('filters.capitalize', [])
  .filter('capitalize', function() {
    return function(word) {
      return (word) ? word.charAt(0).toUpperCase() + word.substring(1) : '';
    };
  });
})();

Save that and our test should be passing. To use this in our app let's add it to our index.html file and add it as a dependency to our app.js file.

Save that and our test should be passing. To use this in our app let's add it to our index.html file and add it as a dependency to our app.js file.

<head>
  ...
  ...
  <script src="filters/capitalize/capitalize.js"></script>

  <script src="app.js"></script>
</head>
(function() {
  'use strict';

  angular.module('meetIrl', [
    'ui.router',
    'api.users',
    'api.pokemon',
    'components.users',
    'components.profile',
    'components.missingno',
    'filters.capitalize'
  ])
  .config(function($urlRouterProvider) {
    $urlRouterProvider.otherwise('/users');
  });
})();

Now we can update our profile page at /components/profile/profile.html to use our new capitalize filter.

Now we can update our profile page at /components/profile/profile.html to use our new capitalize filter.

...<div class="panel-body text-center">
      <div><span class="glyphicon glyphicon-briefcase" aria-hidden="true"></span> {{pc.user.role}}</div>
      <div><span class="glyphicon glyphicon-map-marker" aria-hidden="true"></span> {{pc.user.location}}</div>
      <div><span class="glyphicon glyphicon-link" aria-hidden="true"></span> {{pc.user.twitter}}</div>
      <div><span class="glyphicon glyphicon-leaf" aria-hidden="true"></span> {{pc.user.pokemon.name | capitalize}}</div>
      <div><span class="glyphicon glyphicon-tag" aria-hidden="true"></span> {{pc.user.pokemon.type | capitalize}}</div>
    </div>
...

翻译自: https://scotch.io/tutorials/testing-angularjs-with-jasmine-and-karma-part-2

karma jasmine

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值