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





5.00/5 (18投票s)
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.AspNetCore
、Microsoft.AspNetCore.Mvc
、Microsoft.EntityFrameworkCore
和 Microsoft.Extensions.DependencyInjection
。
除了这些常用包之外,我们还需要添加 Microsoft.EntityFrameworkCore.InMemory
、NSubstitute
、Shouldly
和 TestStack.BDDfy
。
然后添加对另外两个项目 GlobalWeather
和 Weather.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 的好处之一是它可以帮助提高测试代码的可读性。它通过两种方式实现这一点
- 消除预期值和实际值的歧义,以及
- 生成流畅可读的代码。
运行和调试单元测试
运行后,您可以在测试资源管理器中看到结果。
现在,我们添加其他测试: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 WeatherComponent
、import ReactiveFormsModule
和 import 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);
});
使用 expectOne
、expectNone
或 match
模拟请求。
我们准备模拟数据
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
,但 weatherForm
是 private
的。所以只需在 weather.component.ts 中删除 weatherForm
的 private
即可。
替换
private weatherForm: FormGroup;
用
weatherForm: FormGroup;
这里,我们有两个测试用例来验证响应式表单。回到 weather.component.ts,City
字段是必需的。
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日:初始版本