In this article, I'll show you how to use BDDfy in xUnit for .NET Core, and how to create and debug unit test for Angular.
Introduction
In Global Weather Part 1 and Global Weather Part 2, we built an Angular 7 app and a .NET Core 2.2 micro API service step by step. In this article, we’ll start to look at unit tests. I'll show you how to use BDDfy
in xUnit for .NET Core. Also, I'll show you how to create and debug unit test for Angular.
Unit Test
Having automated tests is a great way to ensure a software application does what its authors intend it to do. There are multiple types of tests for software applications. These include integration tests, web tests, load tests, and others. Unit tests test individual software components and methods. Unit tests should only test code within the developer’s control. They should not test infrastructure concerns. Infrastructure concerns include databases, file systems, and network resources.
Test Driven Development (TDD) is when a unit test is written before the code it is meant to check. TDD is like creating an outline for a book before we write it. It is meant to help developers write simpler, more readable, and efficient code.
Obviously, Global Weather articles are not following TDD. Anyway TDD is not our topic here.
Unit Testing In .NET Core
Create xUnit Test Project
xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET and Xamarin.
Now I show you how to create xUnit test project for ASP .NET Core. In Solution Explorer, add new project, Weather.Test
.
Select “xUnit Test Project (.NET Core)” template and name the project “Weather.Test
”. Click “OK”. Weather.Test
project is created under GlobalWeather
solution.
Delete UnitTest1.cs. Right click Weather.Test
project to select “Manage Nuget Packages”.
Add Micorsoft.AspNetCore
, Microsoft.AspNetCore.Mvc
, Microsoft.EntityFrameworkCore
, and Microsoft.Extensions.DependencyInjection
.
Besides these common packages, we need to add Microsoft.EntityFrameworkCore.InMemory
, NSubstitute
, Shouldly
and TestStack.BDDfy
.
Then add reference to the other two projects, GlobalWeather
and Weather.Persistence
.
What's Bddfy?
BDDfy
is the simplest BDD framework for .NET. The name comes from the fact that it allows you to turn your tests into BDD behaviours simply. What’s the BDD behaviour?
In simple words, BDD behaviour is Given, When and Then.
Given-When-Then is a style of representing tests - or as its advocates would say - specifying a system's behaviour using SpecificationByExample
.
The essential idea is to break down writing a scenario (or test) into three sections:
The
given part describes the state of the world before you begin the behaviour you're specifying in this scenario. You can think of it as the pre-conditions to the test.
The when section is that behaviour that you're specifying.
Finally, the then
section describes the changes you expect due to the specified behaviour.
Unit Testing Repository Generic Class
Right click Weather.Test
project, add Persistence folder. Because persistence tests need a mock database, create MockDatabaseHelper
class with Microsoft.EntityFrameworkCore.InMemory
.
public static class MockDatabaseHelper
{
public static DbContextOptions<WeatherDbContext>
CreateNewContextOptions(string databaseName)
{
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var builder = new DbContextOptionsBuilder<WeatherDbContext>();
builder.UseInMemoryDatabase(databaseName)
.UseInternalServiceProvider(serviceProvider);
return builder.Options;
}
}
We first create unit test for the generic repository class. Create a new C# file named RepositoryTest.cs. Add the following code:
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) };
}
}
Then add test cases. The [Fact]
attribute indicates a test method that is run by the test runner.
The first test is testing if creating a new city in database correctly.
#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
method is a setup step that creates the database context in memory. GivenTheDatabaseHasCities
method is a setup step that adds a city
entry in Cities
table. WhenCreateIsCalledWithTheCityAsync
method is a considered state transition step which calls AddEntity
method. ThenItShouldReturnTheCity
method is asserting step.
In this test, we are using NSubstitute and Shouldly.
NSubstitute and Shouldly
NSubstitute is a friendly substitute for .NET mocking frameworks.
When you write unit tests, you occasionally need to mock away dependencies of the subject under test (SUT). By far, the easiest way to do this is to use a mocking library, which has the added benefit that it allows you to verify the behaviour of the SUT by inspecting its interaction with the mock.
NSubstitute and Moq are two most popular .NET mocking frameworks. However, NSubstitute has a much cleaner syntax than Moq, and it supports the context/specification style out of the box.
Shouldly is another testing framework, which improved test code readability and has better test failure messages. One of the benefits of Shouldly is that it can help to improve the readability of test code. It does this in two ways:
- Disambiguates expected and actual values, and
- Produces fluently readable code.
Run and Debug Unit Test
After it runs, you can see the result in Test Explorer.
Now, we add other tests: CreateCityShouldThrowException()
, GetCityShouldSucceed()
, UpdateCityShouldSucceed()
and 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);
}
Unit Test for API Controller
Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as filters, routing, and model binding. Tests that cover the interactions among components that collectively respond to a request are handled by integration tests.
Create “Controllers” folder in Weather.Test
project. Add a class named CitiesController.cs, and replace the code with the following code:
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
}
}
As explained before, in controller unit test, we mock service with substitute. Then write tests for http get
and http post
.
In the above code, we use _service.Received().UpdateLastAccessedCityAsync(_testData)
. In some cases (particularly for void
methods), it is useful to check that a specific call has been received by a substitute. This can be checked using the Received()
extension method, followed by the call being checked.
Run Tests in Visual Studio 2017
You are now ready to run the tests. All the methods that are marked with the [Fact]
attribute will be tested. From the Test menu item, run the tests.
Open the Test Explorer window, and notice the results of the tests.
Unit Testing In Angular 7
Here, we are going to use Jasmine and Karma to test our Angular 7 Application.
Jasmine
Jasmine is an open source testing framework for JavaScript.
Before starting, you need to understand the basics of Jasmine.
describe
- is the function that has the collection of individual test spec test spec
- it just has one or more test expectation
Before performing or after having performed our test case, we need to insert some mock data or we need to do some cleaning activity. For these purposes, we have:
beforeAll
- This function is called once before all the specs in the test suite are run. afterAll
- This function is called once after all the specs in a test suite are finished. beforeEach
- This function is called before each test specs. afterEach
- This function is called after each test specs.
Karma
It is just a test runner. It is a tool which lets us spawn browsers and run jasmine tests inside of them all from the command line. The results of the tests are also displayed on the command line.
Write Unit Test Spec In Angular 7
The Angular CLI downloads and install everything you need to test an Angular application with the Jasmine test framework.
When we use Angular CLI command to create component and service, default test specs are already created. For example, 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!');
});
});
If you run ng test
, karma will open your browser where you can see test results. Start PowerShell, go to GlobalWeather\GlobalWeather\WeatherClient folder. Run the below command:
ng test
Karma opens your browser, I assume you set Chrome as the default browser.
You can see all unit tests failed. But don’t panic. Most errors are caused by modules not imported properly. Let’s make test specs work. Firstly, start from app.component.spec.ts.
Unit Testing App Component
We change app.component.spec.ts like below:
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');
});
});
If you compare it to the previous code, you can see the major change is fixing the imports, like import WeatherComponent
, import ReactiveFormsModule
, and import NgbMoudle
. Also, besides the default test case, “should create the app”, adds a new one, “should have as title 'WeatherClient
'”.
Let’s run the test again by "ng test
".
Look, all errors in app.component.spec.ts is gone, which means app.component.ts pass the test.
Unit Testing City Service
Next, we fix cityservice.spec.ts, replace the default code with below code:
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);
});
});
Here, one thing that needs mention is how to test http get service.
Setup
We setup the TestBed
importing the HttpClientTestingModule
and providing HttpTestingController
. Of course, we also provide the service we're testing, CityService
.
We also run HttpTestingController#verify
to make sure that there are no outstanding requests:
afterEach(() => { httpTestingController.verify(); });
Mocking
You can use the HttpTestingController
to mock requests and the flush
method to provide dummy values as responses. As the HTTP request methods return an Observable
, we subscribe to it and create our expectations in the callback methods:
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);
});
Mock the request using expectOne
, expectNone
or match
.
We prepare the mock data:
const result = {id: '26216', name: 'Melbourne', countryId: 'AU'} as CityMetaData;
Then, flush
this mock data to http request.
req.flush(result);
Unit Testing Current Conditions Service
Fix current-conditions.service.spec.ts. Replace the default code with the below:
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);
});
});
Unit Testing Location Service
Fix location.service.spec.ts. Replace the default code with the below:
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);
});
});
Unit Testing Weather Component
Fix weather.component.spec.ts. Replace the default code with the below:
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);
});
});
The above code causes a compile problem, because it tries to access weatherForm
in weather component
, but weatherForm
is private
. So just remove private
for weatherForm
in weather.component.ts.
Replace:
private weatherForm: FormGroup;
with:
weatherForm: FormGroup;
Here, we have two test cases to validate reactive form. Back to weather.component.ts, City
field is required.
buildForm(): FormGroup {
return this.fb.group({
searchGroup: this.fb.group({
country: [
null
],
city: [
null,
[Validators.required]
],
})
});
}
That means if there is no value for City
input field, the form is invalid. Because there is only one required field, the form becomes valid when you enter something in this input.
That behaviour is covered by the below two test cases:
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);
});
});
Wrapping Up
Now we run ng test
again, all test cases passed.
GitHub Repository
I've created GlobalWeather repository on Github. You're most welcome to add more functionality for GlobalWeather
app. Enjoy coding!
Visual Studio 2019
Microsoft has released Visual Studio 2019 on 2nd April. Athough Global Weather solution is created from Visual Studio 2017, it's completely working with Visual Studio 2019.
Conclusion
UNIT TESTING is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. In this article, I discussed how to write a unit test in ASP.NET Core and Angular.
Global Weather series articles cover the whole development cycle of Angular 7 and .NET Core, from front end to back end and to unit tests. There were not many good learning materials for Angular and .NET Core. So I tried to write a cooking book about Angular and .NET Core. But given the limited time, writing a series of articles is a possible solution.
History
- 4th March, 2019: Initial version