65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 7 与 .NET Core 2.2 - 全球天气 (第三部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2019年3月4日

CPOL

9分钟阅读

viewsIcon

40404

downloadIcon

873

Angular 7 与 .NET Core 2.2 - 全球天气

引言

全球天气第一部分全球天气第二部分中,我们逐步构建了一个 Angular 7 应用程序和一个 .NET Core 2.2 微型 API 服务。在本文中,我们将开始探讨单元测试。我将向您展示如何在 xUnit 中使用 BDDfy 进行 .NET Core 开发。此外,我将向您展示如何为 Angular 创建和调试单元测试。

单元测试

自动化测试是确保软件应用程序按其作者意图运行的好方法。软件应用程序有多种类型的测试。这些包括集成测试、Web 测试、负载测试等。单元测试测试单个软件组件和方法。单元测试应该只测试开发人员控制范围内的代码。它们不应该测试基础设施问题。基础设施问题包括数据库、文件系统和网络资源。

测试驱动开发 (TDD) 是指在编写代码之前先编写用于检查代码的单元测试。TDD 就像我们在写一本书之前先为其创建大纲。它旨在帮助开发人员编写更简单、更具可读性和效率的代码。

显然,《全球天气》系列文章并没有遵循 TDD。无论如何,TDD 不是我们这里的主题。

.NET Core 中的单元测试

创建 xUnit 测试项目

xUnit.net 是一个免费、开源、面向社区的 .NET Framework 单元测试工具。由 NUnit v2 的原始发明者编写,xUnit.net 是用于单元测试 C#、F#、VB.NET 和其他 .NET 语言的最新技术。xUnit.net 与 ReSharper、CodeRush、TestDriven.NET 和 Xamarin 兼容。

现在我将向您展示如何为 ASP.NET Core 创建 xUnit 测试项目。在解决方案资源管理器中,添加新项目 Weather.Test

选择“xUnit 测试项目 (.NET Core)”模板并将项目命名为“Weather.Test”。单击“确定”。Weather.Test 项目在 GlobalWeather 解决方案下创建。

删除 UnitTest1.cs。右键单击 Weather.Test 项目以选择“管理 Nuget 包”。

添加 Microsoft.AspNetCoreMicrosoft.AspNetCore.MvcMicrosoft.EntityFrameworkCoreMicrosoft.Extensions.DependencyInjection

除了这些常用包之外,我们还需要添加 Microsoft.EntityFrameworkCore.InMemoryNSubstituteShouldlyTestStack.BDDfy

然后添加对另外两个项目 GlobalWeatherWeather.Persistence 的引用。

什么是 Bddfy?

BDDfy 是 .NET 最简单的 BDD 框架。这个名字来源于它允许您将测试简单地转换为 BDD 行为。什么是 BDD 行为?

简单来说,BDD 行为就是 Given、When 和 Then。

Given-When-Then 是一种表示测试的风格——或者正如其倡导者所说——使用 SpecificationByExample 来指定系统的行为。

基本思想是将编写场景(或测试)分解为三个部分

Given 部分描述了您在此场景中指定行为之前世界的状态。您可以将其视为测试的先决条件。

When 部分是您正在指定的行为。

最后,Then 部分描述了您期望由于指定行为而产生的更改。

单元测试仓库泛型类

右键单击 Weather.Test 项目,添加 Persistence 文件夹。由于持久性测试需要模拟数据库,因此使用 Microsoft.EntityFrameworkCore.InMemory 创建 MockDatabaseHelper 类。

public static class MockDatabaseHelper
{
    public static DbContextOptions<WeatherDbContext> 
           CreateNewContextOptions(string databaseName)
    {
        //Create a fresh service provider, and therefore a fresh    
        // InMemory database instance    
        var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();
    
        // Create a new options instance telling the context to use an    
        // InMemory database and the new service provider    
        var builder = new DbContextOptionsBuilder<WeatherDbContext>();    
        builder.UseInMemoryDatabase(databaseName)    
            .UseInternalServiceProvider(serviceProvider);    
        return builder.Options;
    }
}

我们首先为泛型仓库类创建单元测试。创建一个名为 RepositoryTest.cs 的新 C# 文件。添加以下代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Weather.Persistence.Config;
using Weather.Persistence.Models;
using Weather.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NSubstitute;
using Serilog;
using Shouldly;
using TestStack.BDDfy;
using Xunit;

namespace Weather.Test.Persistence
{
    public class RepositoryTest
    {
        private DbContextOptions<WeatherDbContext> _contextOptions;
        private City _testData;
        private WeatherDbContext _appContext;
        private IOptions<DbContextSettings> _settings;
        private IDbContextFactory _dbContextFactory;
        private Repository<City> _subject;
        private City _result;

        public RepositoryTest()
        {
            _testData = new City { Id = "26216", Name = "Melbourne", 
                        CountryId = "AU", AccessedDate = 
                                     new DateTime(2018, 12, 29, 10, 1, 2) };

        }
}

然后添加测试用例。[Fact] 属性表示由测试运行器运行的测试方法。

第一个测试是测试是否在数据库中正确创建新城市。

#region Facts
[Fact]
public void CreateCityShouldSucceed()
{
    this.Given(x => GivenADatabase("TestDb"))
        .And(x => GivenTheDatabaseHasCities(1))
        .When(x => WhenCreateIsCalledWithTheCityAsync(_testData))
        .Then(x => ThenItShouldReturnTheCity(_testData))
        .BDDfy();
}

#endregion

#region Givens
private void GivenADatabase(string context)
{
    _contextOptions = MockDatabaseHelper.CreateNewContextOptions(context);
    _appContext = new WeatherDbContext(_contextOptions);
    _settings = Substitute.For<IOptions<DbContextSettings>>();

    _settings.Value.Returns(new DbContextSettings { DbConnectionString = "test" });
    _dbContextFactory = Substitute.For<IDbContextFactory>();
    _dbContextFactory.DbContext.Returns(_appContext);
    _subject = new Repository<City>(_dbContextFactory, Substitute.For<ILogger>());
}

private void GivenTheDatabaseHasCities(int numberOfCities)
{
    var cities = new List<City>();
    for (var item = 0; item < numberOfCities; item++)
    {
        cities.Add(
            new City()
            {
                Id = (item + 1).ToString(),
                Name = $"City{item}",
                CountryId = "AU",
                AccessedDate = DateTimeOffset.UtcNow,
            }
        );
    }

    _appContext.Cities.AddRange(cities);
    _appContext.SaveChanges();
}
#endregion

#region Whens
private async Task<bool> WhenCreateIsCalledWithTheCityAsync(City city)
{
    _result = await _subject.AddEntity(city);
    return true;
}
#endregion

#region Thens
private void ThenItShouldReturnTheCity(City city)
{
    _result.Id.ShouldBe(city.Id);
}
#endregion
  • GivenADatabase 方法是一个设置步骤,用于在内存中创建数据库上下文。
  • GivenTheDatabaseHasCities 方法是一个设置步骤,用于在 Cities 表中添加一个 city 条目。
  • WhenCreateIsCalledWithTheCityAsync 方法是一个被认为是状态转换的步骤,它调用 AddEntity 方法。
  • ThenItShouldReturnTheCity 方法是一个断言步骤。

在此测试中,我们使用 NSubstitute 和 Shouldly。

NSubstitute 和 Shouldly

NSubstitute 是 .NET 模拟框架的友好替代品。

当您编写单元测试时,偶尔需要模拟被测主题 (SUT) 的依赖项。到目前为止,最简单的方法是使用模拟库,它具有额外的优点,即它允许您通过检查其与模拟的交互来验证 SUT 的行为。

NSubstitute 和 Moq 是两个最流行的 .NET 模拟框架。然而,NSubstitute 比 Moq 具有更简洁的语法,并且它开箱即支持上下文/规范样式。

Shouldly 是另一个测试框架,它提高了测试代码的可读性并具有更好的测试失败消息。Shouldly 的好处之一是它可以帮助提高测试代码的可读性。它通过两种方式实现这一点

  1. 消除预期值和实际值的歧义,以及
  2. 生成流畅可读的代码。

运行和调试单元测试

运行后,您可以在测试资源管理器中看到结果。

现在,我们添加其他测试:CreateCityShouldThrowException()GetCityShouldSucceed()UpdateCityShouldSucceed()DeleteCityShouldSucceed()

CreateCityShouldThrowException:

[Fact]
public void CreateCityShouldThrowException()
{
    this.Given(x => GivenADatabase("TestDb"))
        .Given(x => GivenTheDatabaseHasACity(_testData))
        .When(x => WhenCreateSameIdIsCalledWithTheCityAsync(_testData))
        .Then(x => ThenItShouldBeSuccessful())
        .BDDfy();
}
private void GivenTheDatabaseHasACity(City city)
{
    _appContext.Cities.Add(city);
    _appContext.SaveChanges();
}
private async Task WhenCreateSameIdIsCalledWithTheCityAsync(City city)
{
    await Assert.ThrowsAsync<ArgumentException>
                 (async () => await _subject.AddEntity(city));
}
private void ThenItShouldBeSuccessful()
{ }

GetCityShouldSucceed:

[Fact]
public void GetCityShouldSucceed()
{
    this.Given(x => GivenADatabase("TestDb"))
        .Given(x => GivenTheDatabaseHasACity(_testData))
        .When(x => WhenGetCalledWithTheCityIdAsync(_testData.Id))
        .Then(x => ThenItShouldReturnTheCity(_testData))
        .BDDfy();
}
private async Task<bool> WhenGetCalledWithTheCityIdAsync(string id)
{
    _result = await _subject.GetEntity(id);
    return true;
}

UpdateCityShouldSucceed:

[Fact]
public void UpdateCityShouldSucceed()
{
    var city = new City
    {
        Id = _testData.Id,
        Name = "Melbourne",
        CountryId = "AU",
        AccessedDate = new DateTime(2018, 12, 30, 10, 1, 2)
    };
    this.Given(x => GivenADatabase("TestDb"))
        .Given(x => GivenTheDatabaseHasACity(_testData))
        .When(x => WhenUpdateCalledWithTheCityAsync(city))
        .Then(x => ThenItShouldReturnTheCity(city))
        .BDDfy();
}
private async Task<bool> WhenUpdateCalledWithTheCityAsync(City city)
{
    var entity = await _subject.GetEntity(city.Id);
    entity.Name = city.Name;
    entity.CountryId = city.CountryId;
    entity.AccessedDate = city.AccessedDate;
    _result = await _subject.UpdateEntity(entity);
    return true;
}

DeleteCityShouldSucceed:

[Fact]
public void DeleteCityShouldSucceed()
{
    this.Given(x => GivenADatabase("TestDb"))
        .Given(x => GivenTheDatabaseHasACity(_testData))
        .When(x => WhenDeleteCalledWithTheCityIdAsync(_testData.Id))
        .Then(x => ThenItShouldBeNoExistCity())
        .BDDfy();
}
private async Task<bool> WhenDeleteCalledWithTheCityIdAsync(string id)
{
    await _subject.DeleteEntity(id);
    return true;
}
private void ThenItShouldBeNoExistCity()
{
    _appContext.Cities.Count().ShouldBe(0);
}

API 控制器的单元测试

设置控制器操作的单元测试以关注控制器的行为。控制器单元测试避免了过滤器、路由和模型绑定等场景。涵盖组件之间协同响应请求的交互的测试由集成测试处理。

Weather.Test 项目中创建“Controllers”文件夹。添加一个名为 CitiesController.cs 的类,并用以下代码替换代码

using System;
using System.Threading.Tasks;
using GlobalWeather.Controllers;
using GlobalWeather.Services;
using NSubstitute;
using Serilog;
using TestStack.BDDfy;
using Xunit;
using Microsoft.AspNetCore.Mvc;
using Weather.Persistence.Models;

namespace Weather.Test.Controllers
{
    public class CitiesControllerTest
    {
        private ICityService _service;
        private CitiesController _controller;
        private City _testData;
        private ActionResult<City> _result;

        #region Facts
        [Fact]
        public void GetReturnsExpectedResult()
        {
            this.Given(x => GivenCitiesControllerSetup())
                .And(x => GivenGeLastAccessedCityReturnsExpected())
                .When(x => WhenGetCalledAsync())
                .Then(x => ThenResultShouldBeOk())
                .BDDfy();
        }

        [Fact]
        public void PostCallService()
        {
            this.Given(x => GivenCitiesControllerSetup())
                .When(x => WhenPostCalledAsync())
                .Then(x => ThenItShouldCallUpdateAccessedCityInService())
                .BDDfy();
        }
        #endregion

        #region Gievns

        private void GivenCitiesControllerSetup()
        {
            _testData = new City
            { Id = "26216", Name = "Melbourne", 
              CountryId = "AU", AccessedDate = DateTimeOffset.UtcNow };
            _service = Substitute.For<ICityService>();
            _controller = new CitiesController(_service, Substitute.For<ILogger>());
        }

        private void GivenGeLastAccessedCityReturnsExpected()
        {
            _service.GetLastAccessedCityAsync().Returns(new City());
        }

        #endregion

        #region Whens
        private async Task WhenGetCalledAsync()
        {
            _result = await _controller.Get();
        }

        private async Task WhenPostCalledAsync()
        {
            await _controller.Post(_testData);
        }
        #endregion

        #region Thens
        private void ThenResultShouldBeOk()
        {
            Assert.NotNull(_result);
            Assert.IsType<City>(_result.Value);
        }

        private void ThenItShouldCallUpdateAccessedCityInService()
        {
            _service.Received().UpdateLastAccessedCityAsync(_testData);
        }
        #endregion
    }
}

如前所述,在控制器单元测试中,我们使用替代品模拟服务。然后编写 http get 和 http post 的测试。

在上面的代码中,我们使用 _service.Received().UpdateLastAccessedCityAsync(_testData)。在某些情况下(特别是对于 void 方法),检查特定调用是否已被替代品接收是有用的。这可以使用 Received() 扩展方法进行检查,然后是正在检查的调用。

在 Visual Studio 2017 中运行测试

您现在可以运行测试了。所有标有 [Fact] 属性的方法都将进行测试。从测试菜单项运行测试。

打开测试资源管理器窗口,并注意测试结果。

Angular 7 中的单元测试

在这里,我们将使用 Jasmine 和 Karma 来测试我们的 Angular 7 应用程序。

Jasmine

Jasmine 是一个用于 JavaScript 的开源测试框架。

在开始之前,您需要了解 Jasmine 的基础知识。

  • describe - 是包含单个测试规范集合的函数
  • test spec - 它只包含一个或多个测试预期

在执行或执行测试用例之后,我们需要插入一些模拟数据或进行一些清理活动。为此,我们有

  • beforeAll - 此函数在测试套件中的所有规范运行之前调用一次。
  • afterAll - 此函数在测试套件中的所有规范完成后调用一次。
  • beforeEach - 此函数在每个测试规范之前调用。
  • afterEach - 此函数在每个测试规范之后调用。

Karma

它只是一个测试运行器。它是一个工具,让我们可以在命令行中生成浏览器并在其中运行 jasmine 测试。测试结果也显示在命令行中。

在 Angular 7 中编写单元测试规范

Angular CLI 下载并安装您需要使用 Jasmine 测试框架测试 Angular 应用程序的所有内容。

当我们使用 Angular CLI 命令创建组件和服务时,默认的测试规范已经创建。例如,app.component.spec.ts

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'WeatherClient'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('WeatherClient');
  });

  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain
                                        ('Welcome to WeatherClient!');
  });
});

如果您运行 ng test,Karma 将打开您的浏览器,您可以在其中查看测试结果。启动 PowerShell,转到 GlobalWeather\GlobalWeather\WeatherClient 文件夹。运行以下命令

ng test

Karma 会打开您的浏览器,我假设您将 Chrome 设置为默认浏览器。

您可以看到所有单元测试都失败了。但不要惊慌。大多数错误都是由模块未正确导入引起的。让我们使测试规范工作。首先,从 app.component.spec.ts 开始。

单元测试 App 组件

我们修改 app.component.spec.ts 如下

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AppComponent } from './app.component';
import { WeatherComponent } from './weather/weather.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
        ReactiveFormsModule,
        NgbModule
      ],
      declarations: [
        AppComponent,
        WeatherComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'WeatherClient'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('WeatherClient');
  });

});

如果您将其与之前的代码进行比较,您会发现主要变化是修复了导入,例如 import WeatherComponentimport ReactiveFormsModuleimport NgbMoudle。此外,除了默认的测试用例“应该创建应用程序”之外,还添加了一个新的测试用例,“应该将标题设为 'WeatherClient'”。

让我们再次通过 "ng test" 运行测试。

看,app.component.spec.ts 中的所有错误都消失了,这意味着 app.component.ts 通过了测试。

单元测试城市服务

接下来,我们修复 cityservice.spec.ts,用以下代码替换默认代码

import { async, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } 
                                    from '@angular/common/http/testing';
import { Constants } from '../../../app/app.constants';
import { CityService } from './city.service';
import { ErrorHandleService } from './error-handle.service';
import { CityMetaData } from '../models/city-meta-data';
import { City } from '../models/city';

describe('CityService', () => {
  let service: CityService;
  let httpTestingController: HttpTestingController;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [CityService, ErrorHandleService]
    });
    service = TestBed.get(CityService);
    httpTestingController = TestBed.get(HttpTestingController);
  }));

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create', () => {
    expect(service).toBeTruthy();
  });

  it('should get last accessed city', () => {
    const result = { id: '26216', name: 'Melbourne', countryId: 'AU' } as CityMetaData;

    service.getLastAccessedCity()
      .subscribe(
        (data: City) => expect(data.Key).toEqual('26216'),
        (err) => expect(err).toBeNull()
      );
    const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
    const req: TestRequest = 
          httpTestingController.expectOne(req => req.url.includes(uri));

    expect(req.request.method).toEqual('GET');

    req.flush(result);
  });
});

这里,有一点需要提到的是如何测试 http get 服务。

安装

我们设置 TestBed,导入 HttpClientTestingModule 并提供 HttpTestingController。当然,我们还提供了我们正在测试的服务 CityService

我们还运行 HttpTestingController#verify 以确保没有未完成的请求

afterEach(() => { httpTestingController.verify(); });

模拟

您可以使用 HttpTestingController 模拟请求,并使用 flush 方法提供虚拟值作为响应。由于 HTTP 请求方法返回一个 Observable,我们订阅它并在回调方法中创建我们的预期

it('should get last accessed city', () => {
  const result = { id: '26216', name: 'Melbourne', countryId: 'AU' } as CityMetaData;

  service.getLastAccessedCity()
    .subscribe(
      (data: City) => expect(data.Key).toEqual('26216'),
      (err) => expect(err).toBeNull()
    );
  const uri = decodeURIComponent(`${Constants.cityAPIUrl}`);
  const req: TestRequest = httpTestingController.expectOne(req => req.url.includes(uri));

  expect(req.request.method).toEqual('GET');

  req.flush(result);
});

使用 expectOneexpectNonematch 模拟请求。

我们准备模拟数据

const result = {id: '26216', name: 'Melbourne', countryId: 'AU'} as CityMetaData;

然后,将此模拟数据 flush 到 http 请求。

req.flush(result);

单元测试当前状况服务

修复 current-conditions.service.spec.ts。用以下代码替换默认代码

import { async, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } 
                                          from '@angular/common/http/testing';
import { Constants } from '../../../app/app.constants';
import { CurrentConditionsService } from './current-conditions.service';
import { ErrorHandleService } from './error-handle.service';
import { CurrentConditions } from '../models/current-conditions';

describe(' CurrentConditionsService', () => {
  let service: CurrentConditionsService;
  let httpTestingController: HttpTestingController;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [CurrentConditionsService, ErrorHandleService]
    });
    service = TestBed.get(CurrentConditionsService);
    httpTestingController = TestBed.get(HttpTestingController);
  }));

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create',
    () => {
      expect(service).toBeTruthy();
    });

  it('should get current conditions',
    () => {
      const result = [
        {
          LocalObservationDateTime: '',
          WeatherText: 'Sunny',
          WeatherIcon: 1,
          IsDayTime: true,
          Temperature: {
            Imperial: null,
            Metric: {
              Unit: 'C',
              UnitType: 1,
              Value: 36
            }
          }
        }
      ] as CurrentConditions[];

      service.getCurrentConditions('26216')
        .subscribe(
          (data: CurrentConditions[]) => expect
                 (data.length === 1 && data[0].WeatherText === 'Sunny').toBeTruthy(),
          (err: CurrentConditions[]) => expect(err.length).toEqual(0)
        );
      const uri = decodeURIComponent(`${Constants.currentConditionsAPIUrl}/
                                                  26216?apikey=${Constants.apiKey}`);
      const req: TestRequest = 
            httpTestingController.expectOne(req => req.url.includes(uri));

      expect(req.request.method).toEqual('GET');

      req.flush(result);
    });
});

单元测试位置服务

修复 location.service.spec.ts。用以下代码替换默认代码

import { async, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } 
                                         from '@angular/common/http/testing';
import { Constants } from '../../../app/app.constants';
import { LocationService } from './location.service';
import { ErrorHandleService } from './error-handle.service';
import { Country } from '../../shared/models/country';
import { City } from '../../shared/models/city';

describe('LocationService', () => {
  let service: LocationService;
  let httpTestingController: HttpTestingController;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [LocationService, ErrorHandleService]
    });
    service = TestBed.get(LocationService);
    httpTestingController = TestBed.get(HttpTestingController);
  }));

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create', () => {
    expect(service).toBeTruthy();
  });

  it('should get location', () => {
    const result = [{
      Key: '26216', EnglishName: 'Melbourne', Type: 'City', Country: {
        ID: 'AU',
        EnglishName: 'Australia'
      }
    }] as City[];

    service.getCities('melbourne', 'AU')
      .subscribe(
        (data: City[]) => expect(data.length === 1 && 
         data[0].Key === '26216').toBeTruthy(),
        (err: City[]) => expect(err.length).toEqual(0)
      );
    const uri = decodeURIComponent(
      `${Constants.locationAPIUrl}/cities/AU/search?
         apikey=${Constants.apiKey}&q=melbourne`);
    const req: TestRequest = 
          httpTestingController.expectOne(req => req.url.includes(uri));

    expect(req.request.method).toEqual('GET');

    req.flush(result);
  });

  it('should get countries', () => {
    const result = [{
      ID: 'AU', EnglishName: 'Australia'
    }] as Country[];

    service.getCountries()
      .subscribe(
        (data: Country[]) => expect(data.length === 1 && 
                             data[0].ID === 'AU').toBeTruthy(),
        (err: Country[]) => expect(err.length).toEqual(0)
      );
    const uri = decodeURIComponent
          (`${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`);
    const req: TestRequest = httpTestingController.expectOne
                             (req => req.url.includes(uri));

    expect(req.request.method).toEqual('GET');

    req.flush(result);
  });
});

单元测试天气组件

修复 weather.component.spec.ts。用以下代码替换默认代码

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ReactiveFormsModule, FormGroup, FormControl, FormBuilder, Validators } 
         from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { WeatherComponent } from './weather.component';
import { LocationService } from '../shared/services/location.service';
import { CurrentConditionsService } from '../shared/services/current-conditions.service';
import { CityService } from '../shared/services/city.service';
import { ErrorHandleService } from '../shared/services/error-handle.service';

describe('WeatherComponent', () => {
  let component: WeatherComponent;
  let fixture: ComponentFixture<WeatherComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [WeatherComponent],
      imports: [ReactiveFormsModule, NgbModule, 
                RouterTestingModule, HttpClientTestingModule],
      providers: [LocationService, CurrentConditionsService, 
                  CityService, ErrorHandleService]
    })
      .compileComponents();
    fixture = TestBed.createComponent(WeatherComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }));

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should get invalid form when location field is empty ',
    () => {
      component.ngOnInit();
      expect(component.weatherForm.valid).toEqual(false);
    });

  it('should get valid form when location field has value ',
    () => {
      component.ngOnInit();
      component.cityControl.patchValue("something");
      expect(component.weatherForm.valid).toEqual(true);
    });
});

上面的代码会导致编译问题,因为它试图访问 weather component 中的 weatherForm,但 weatherFormprivate 的。所以只需在 weather.component.ts 中删除 weatherFormprivate 即可。

替换

private weatherForm: FormGroup;

weatherForm: FormGroup;

这里,我们有两个测试用例来验证响应式表单。回到 weather.component.tsCity 字段是必需的。

buildForm(): FormGroup {
  return this.fb.group({
    searchGroup: this.fb.group({
      country: [
        null
      ],
      city: [
        null,
        [Validators.required]
      ],
    })
  });
}

这意味着如果 City 输入字段没有值,则表单无效。因为只有一个必填字段,当您在此输入中输入内容时,表单将变为有效。

该行为由以下两个测试用例涵盖

it('should get invalid form when location field is empty ',
  () => {
    component.ngOnInit();
    expect(component.weatherForm.valid).toEqual(false);
  });

it('should get valid form when location field has value ',
  () => {
    component.ngOnInit();
    component.cityControl.patchValue("something");
    expect(component.weatherForm.valid).toEqual(true);
  });
});

总结

现在我们再次运行 ng test,所有测试用例都通过了。

GitHub 存储库

我已经在 Github 上创建了 GlobalWeather 仓库。非常欢迎您为 GlobalWeather 应用程序添加更多功能。享受编码的乐趣!

Visual Studio 2019

Microsoft 已于 4 月 2 日发布了 Visual Studio 2019。虽然 Global Weather 解决方案是使用 Visual Studio 2017 创建的,但它完全兼容 Visual Studio 2019。

结论

单元测试是软件测试的一个级别,用于测试软件的各个单元/组件。目的是验证软件的每个单元是否按设计执行。在本文中,我讨论了如何在 ASP.NET Core 和 Angular 中编写单元测试。

全球天气系列文章涵盖了 Angular 7 和 .NET Core 的整个开发周期,从前端到后端再到单元测试。关于 Angular 和 .NET Core 的优秀学习资料不多。所以我尝试写一本关于 Angular 和 .NET Core 的烹饪书。但鉴于时间有限,编写一系列文章是一个可行的解决方案。

历史

  • 2019年3月4日:初始版本
© . All rights reserved.