NetCoreHeroes: Angular 2 with .Net Core in Visual Studio 2015, Part III

Georg Dangl by Georg Dangl in Web Development Tuesday, July 26, 2016

Tuesday, July 26, 2016

Posted in DotNet Angular2

The source code for this project is available at GitHub and here's a live example of the website.

This third part of the series is focusing on implementing unit tests for both Asp.Net Core and Angular 2 that can be run in the Visual Studio Test Runner. You should have read part one (how to setup the dev environment) and part two (the Tour of Heroes Angular 2 tutorial in Visual Studio) to get you started.

Right now, it's on .Net Core 1.0.0 1.0.1 (You should upgrade!) 1.1.0 1.1.2 2.0 2.0 and Angular 2 RC3 RC4 RC5 RC6 Final 4.1.3 5.2.3, but I will write about migrations when there are updates. You can read about what I had to change for each migration in the commits on GitHub.

Unit Testing Asp.Net Core in Visual Studio

In .Net Core, xUnit is the test framework of choice. It's really powerful, easy to use and has great tooling support, so we'll test our app with it!

To separate the test from the actual code, just add a new .Net Core Class Library project to your solution. In this tutorial, I'm doing a single unit test on the HeroesController to check if it does actually return my heroes.

First, this is how your test projects csproj should look like:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
    <AssemblyName>NetCoreHeroes.Tests</AssemblyName>
    <PackageId>NetCoreHeroes.Tests</PackageId>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <RuntimeFrameworkVersion>1.1.2</RuntimeFrameworkVersion>
    <PackageTargetFallback>$(PackageTargetFallback);dotnet5.6;portable-net45+win8</PackageTargetFallback>
    <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
    <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
    <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\src\NetCoreHeroes\NetCoreHeroes.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170517-02" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.3.0-beta2-build1317" />
    <PackageReference Include="xunit" Version="2.3.0-beta2-build3683" />
    <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.0-beta2-build3683" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="1.1.2" />
    <PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.0" />
  </ItemGroup>
  <ItemGroup>
    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
  </ItemGroup>
</Project>

With this setup, you'll be able to use the InMemory database context for unit testing. Additionally, the dotnet-xunit CLI tool and the xunit.runner.visualstudio packages already integrate with the Visual Studio Test Runner and the dotnet commands, so you can directly start writing test and they'll appear in Visual Studio. Alternatively, you can run them in the project directory via dotnet xunit in the command line. Just make sure to use no earlier version than the ones I use to ensure compatibility with .Net Core 1.1.2.

Attention: Make sure to add the "Microsoft.DotNet.InternalAbstractions": "1.0.0" dependency, that's necessary since xUnit might fail otherwise (and give non meaningful error message in Visual Studio). This will probably be around until you've migrated to Visual Studio 2017 and the new MSBuild based system for .Net Core.

Let's write a first test:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NetCoreHeroes.Controllers;
using NetCoreHeroes.Data;
using Xunit;

namespace NetCoreHeroes.Tests.Controllers
{
    public class HeroesControllerTests
    {
        public class Get : IDisposable
        {
            private readonly HeroesContext _context;
            private readonly HeroesController _controller;
            public Get()
            {
                var services = new ServiceCollection();
                services.AddEntityFrameworkInMemoryDatabase()
                    .AddDbContext<HeroesContext>(x => x.UseInMemoryDatabase().UseInternalServiceProvider(new ServiceCollection().AddEntityFrameworkInMemoryDatabase().BuildServiceProvider())); // Don't share context data -> use new InternalServiceProvider per instance
                _context = services.BuildServiceProvider().GetRequiredService<HeroesContext>();
                _controller = new HeroesController(_context);
            }
            public void Dispose()
            {
                _context?.Dispose();
            }
            [Fact]
            public void ReturnResults()
            {
                // Arrange -> Fill context
                _context.Heroes.Add(new Hero {Name = "Good Guy Greg"});
                _context.Heroes.Add(new Hero {Name = "Bad Luck Brian"});
                _context.Heroes.Add(new Hero {Name = "Doge"});
                _context.SaveChanges();

                // Act
                var response = _controller.Get();

                // Assert
                var objectResult = response as ObjectResult;
                Assert.NotNull(objectResult);
                var heroesList = (objectResult.Value as IEnumerable<Hero>)?.ToList();
                Assert.NotNull(heroesList);
                Assert.Equal("Good Guy Greg", heroesList[0].Name);
                Assert.Equal("Bad Luck Brian", heroesList[1].Name);
                Assert.Equal("Doge", heroesList[2].Name);
            }
        }
    }
}

You'll notice that I name my test classes by appending Tests to the class name and then creating nested classes for the methods I'm actually testing. I find this approach gives me the most readable results in both Visual Studio and Jenkins.

Using a New Data Storage per Test Run with EntityFramework.InMemory

Let's examine the HeroesControllerTests.Get constructor:

public Get()
{
    var services = new ServiceCollection();
    services.AddEntityFrameworkInMemoryDatabase()
        .AddDbContext<HeroesContext>(x => x.UseInMemoryDatabase().UseInternalServiceProvider(new ServiceCollection().AddEntityFrameworkInMemoryDatabase().BuildServiceProvider())); // Don't share context data -> use new InternalServiceProvider per instance
    _context = services.BuildServiceProvider().GetRequiredService<HeroesContext>();
    _controller = new HeroesController(_context);
}

It's creating a new ServiceCollection and adds the EntityFramework.InMemory services to it. In the configuration overload of the AddDbContext method, I'm instructing EntityFramework to use a distinct service provider for its data storage by creating a new ServiceCollection. This becomes relevant when you'll begin to add more unit tests, since without this, the InMemory provider will use a shared data storage throughout your AppDomain and you don't have a clean context. This becomes especially troublesome when you start running your tests in parallel.

Running the Test

If you build your solution and open the test explorer, you should see that the test is being picked up by Visual Studio. Without any plugins, you're ready to unit test your .Net Core application!

Unit Testing Angular 2 with Visual Studio

TypeScript, or for that matter JavaScript in general, feels quite different, especially in terms of tooling, when working as a .Net developer in Visual Studio. It took me quite a bit to figure out how to not only set up the testing environment but to actually get the tests to behave in the way I wanted them. While most JavaScript unit testing seems to be done with Karma, I've found that the development of the Visual Studio extension had it's last commit in 2015. Therefore, I've decided to use the Chutzpah Visual Studio extension. Development there seems much more active and I was actually much faster setting up the Chutzpah environment in comparison to Karma.

After you've installed the Chutzpah extension, add the following two files to your projects wwwroot directory:

 Note: While I usually want to separate actual code from tests in different projects, I'm sticking here to the way the Angular 2 tutorial is built.

chutzpah.json

{
  "Framework": "jasmine",
  "TestHarnessReferenceMode": "AMD",
  "TestHarnessLocationMode": "SettingsFileAdjacent",
  "Tests": [
    {
      "Includes": [ "*.spec.ts" ],
      "Excludes": [ "node_modules" ]
    }
  ],
  "Server": {
    "Enabled": true
  },
  "References": [
    { "Path": "../node_modules/es6-shim/es6-shim.js" },
    { "Path": "../node_modules/core-js/client/shim.min.js" },
    { "Path": "../node_modules/reflect-metadata/Reflect.js" },
    { "Path": "../node_modules/systemjs/dist/system.src.js" },
    { "Path": "../node_modules/zone.js/dist/zone.js" },
    { "Path": "../node_modules/zone.js/dist/async-test.js" },
    { "Path": "../node_modules/zone.js/dist/fake-async-test.js" },
    { "Path": "../node_modules/zone.js/dist/sync-test.js" },
    { "Path": "../node_modules/zone.js/dist/proxy.js" },
    { "Path": "../node_modules/zone.js/dist/jasmine-patch.js" },
    { "Path": "systemjs.config.js" }
  ]
}

This is the configuration for the Chutzpah test runner. Essentially, it's telling Chutzpah to load all files that end with .spec.ts and which references to load. The order of the references here is important!

testSetup.js (This is no longer required if you're at least at Angular 2 RC 5)

// MockBackend does have trouble with umd packages, see https://github.com/angular/angular/issues/9170#issuecomment-229947428 for more info
// Therefore, systemjs is set to packageWithIndex when running unit tests
System.packageWithIndex = true;

Let's create a hero.service.spec.ts file adjacent to the actual HeroService:

// Structure (and most code=) taken from https://raw.githubusercontent.com/angular/angular.io/master/public/docs/_examples/testing/ts/app/http-hero.service.spec.ts at commit abd860c

import { async, inject, TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { MockBackend, MockConnection } from '@angular/http/testing';
import {
    Http,
    XHRBackend,
    RequestMethod,
    Response, ResponseOptions,
    HttpModule
} from '@angular/http';

import { Hero }        from './hero';
import { HeroService } from './hero.service';

// The following initializes the test environment for Angular 2. This call is required for Angular 2 dependency injection.
// New in Angular 2 RC5
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

////////  SPECS  /////////////
describe('HeroService', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                { provide: XHRBackend, useClass: MockBackend },
                HeroService
            ],
            imports: [
                HttpModule
            ]
        });
    });
    describe('save', () => {
        it('should call POST when hero has no id', async(inject([HeroService, XHRBackend], (heroService: HeroService, mockBackend: MockBackend) => {
            var hero = new Hero();
            hero.name = 'George';
            var calledRequestMethod: RequestMethod;
            mockBackend.connections.subscribe((c: MockConnection) => {
                calledRequestMethod = c.request.method;
                hero.id = 5;
                let options = new ResponseOptions({ status: 200, body: hero });
                var response = new Response(options);
                c.mockRespond(response);
            });
            heroService.save(hero)
                .then(() => {
                    expect(calledRequestMethod).toBe(RequestMethod.Post);
                });
        })));
    });
});

I've shortened the code to only cover one test, but you can find the whole project at GitHub.

Before the actual tests can make use of Angular 2 internals like dependency injection, the test environment has to be set up. Starting with Angular 2 RC5, you'll need to initialize it as follows:

TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

See line 20 in hero.service.spec.ts above for the context.

This test just checks that when the HeroServices save method is called, a Http POST is invoked when the hero does not yet have an id specified. This test is already using the async(inject()) pattern to test asynchronous service calls as well as dependency injection.

Note that when using the MockBackend class to mock Http requests, you must inject the type XHRBackend and configure it in the addProviders method to use an instance of MockBackend instead of XHRBackend. If you inject the MockBackend directly, you're not getting run time errors but the Jasmine testing framework is unable to find your test expectations and therefore cannot run the test.

If you're experiencing issues when running the tests from Visual Studio but they run fine in the browser, make sure that the es6-shim dependency is correctly loaded in chutzpah.json.

Running the Test

Just as with the .Net Core tests, build your solution and Chutzpah should pick up your tests in the test explorer window. Your end result should look like this:

Happy testing!

Update 30.08.2016: Updated Angular 2 to RC5

Update 04.09.2016: Updated Angular 2 to RC6

Update 22.09.2016: Updated Angular 2 to 2.0.0 Final and Asp.Net Core to 1.0.1

Update 23.11.2016: Updated Asp.Net Core to 1.1.0

Update 21.02.2017: The demo project is now using webpack as module loader, see here (and on GitHub) how to upgrade

Update 28.05.2017: Updated Angular to 4.1.3 and Asp.Net Core to 1.1.2. Switched to Visual Studio 2017 csproj format

Update 03.02.2018: Updated Angular to 5.2.3 and Asp.Net Core to 2.0


Share this post


comments powered by Disqus

About me

Hi, my name's George! I love coding and blogging about it. I focus on all things around .Net, Web Development and DevOps.

DanglIT

Need a partner for DevOps, Web Services or Software Development?

Contact me at [email protected], +49 (173) 56 45 689 or visit my professional page!

Dangl.Blog();
// Just 💗 Coding

Social Links