Click here to Skip to main content
15,867,308 members
Articles / DevOps / Unit Testing

Angular 7 with .NET Core 2.2 - Global Weather (Part 3)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (20 votes)
9 Apr 2019CPOL9 min read 38.1K   834   53   2
Angular 7 with .NET Core 2.2 - Global Weather
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.

Image 1

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.

Image 2

Select “xUnit Test Project (.NET Core)” template and name the project “Weather.Test”. Click “OK”. Weather.Test project is created under GlobalWeather solution.

Image 3

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.

Image 4

Then add reference to the other two projects, GlobalWeather and Weather.Persistence.

Image 5

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.

C#
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;
    }
}

We first create unit test for the generic repository class. Create a new C# file named RepositoryTest.cs. Add the following code:

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) };

        }
}

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.

C#
#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:

  1. Disambiguates expected and actual values, and
  2. Produces fluently readable code.

Run and Debug Unit Test

Image 6

After it runs, you can see the result in Test Explorer.

Image 7

Now, we add other tests: CreateCityShouldThrowException(), GetCityShouldSucceed(), UpdateCityShouldSucceed() and DeleteCityShouldSucceed().

CreateCityShouldThrowException:

C#
[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:

C#
[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:

C#
[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:

C#
[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:

C#
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.

Image 8

Open the Test Explorer window, and notice the results of the tests.

Image 9

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.

JavaScript
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:

PowerShell
ng test

Karma opens your browser, I assume you set Chrome as the default browser.

Image 10

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:

JavaScript
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".

Image 11

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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

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

Then, flush this mock data to http request.

JavaScript
req.flush(result);

Unit Testing Current Conditions Service

Fix current-conditions.service.spec.ts. Replace the default code with the below:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
private weatherForm: FormGroup;

with:

JavaScript
weatherForm: FormGroup;

Here, we have two test cases to validate reactive form. Back to weather.component.ts, City field is required.

JavaScript
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:

JavaScript
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.

Image 12

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Australia Australia
Fred is a senior software developer who lives in Melbourne, Australia. In 1993, he started Programming using Visual C++, Visual Basic, Java, and Oracle Developer Tools. From 2003, He started with .Net using C#, and then expertise .Net development.

Fred is often working with software projects in different business domains based on different Microsoft Technologies like SQL-Server, C#, VC++, ASP.NET, ASP.Net MVC, WCF,WPF, Silverlight, .Net Core and Angular, although he also did some development works on IBM AS400.

Comments and Discussions

 
QuestionPerformace Pin
vietophb10-Apr-19 21:12
vietophb10-Apr-19 21:12 
GeneralMy vote of 5 Pin
Member 121366198-Mar-19 15:49
Member 121366198-Mar-19 15:49 
Vote 5

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.