30.09.2018 | Sami Ovaska

VARMISTETAAN ETTÄ DEPENDENCY INJECTION ON KONFIGUROITU OIKEIN .NET CORE SOVELLUKSESSA

Aina välillä Dependency Injection konfiguraatioon on tarve tehdä muutoksia (uusia luokkia lisätään, luokkien scopeen on tarve tehdä muutoksia, sovelluksen konfiguraatio muuttuu ja niin edelleen). Käytän .Net corea, joten DI konfiguroidaan Startup.cs -tiedostossa.

Tavoite on rakentaa integraatiotesti joka validoi että dependency injection on konfiguroitu oikein. Testi tulee suorittaa ainakin ennenkuin sovelluksesta julkaistaan uusi version.

Tämä artikkeli on jatkoa artikkelille https://zure.com/blogi/varmista-autentikointi-on-enabloitu-intergraatio-testien-avulla/

Lähdekoodi löytyy https://github.com/sovaska/automaticintegrationtests

DI KONFIGURAATION TESTAAMISEEN KÄYTETTÄVÄN REST ENDPOINTIN RAKENTAMINEN

Ensimmäinen tehtävä on rakentaa REST endpoint jolla testataan dependency injection konfiguraatio. Endpointin nimi on /api/metadata/dependencyinjection. Esimerkkisovelluksessa ei ole autentikointi päällä, mutta jos alla olevaa koodia on tarkoitus käyttää oikeassa sovelluksessa, varmistathan että lisäät autentikaation siihen.

using Microsoft.AspNetCore.Mvc;
using Countries.Web.Contracts;
using System.Collections.Generic;
using Countries.Web.Models;

namespace Countries.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MetadataController : ControllerBase
    {
        private readonly IMetadataService _metadataService;

        public MetadataController(IMetadataService metadataService)
        {
            _metadataService = metadataService;
        }

        [HttpGet("dependencyinjection")]
        public ActionResult<DIMetadata> DependencyInjection()
        {
            var results = _metadataService.GetDependencyInjectionProblems(HttpContext.RequestServices);

            return Ok(results);
        }
    }
}

GetDependencyInjectionProblems() -funktion toteutus löytyy MetadataService -luokasta. Funktio käyttää IServiceProvider:ia injektoimaan kaikki servicet jotka löytyvät IServiceCollection:ista, paitsi ne joilla ContainsGenericParameters on asetettu arvoon true. Jos injektion onnistuu, servicen nimi lisätään ValidTypes -listaan, ja jos se epäonnistuu, servicen nimi ja epäonnistumisen syy lisätään Problems -listaan.

using System.Linq;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Countries.Web.Contracts;
using Countries.Web.Models;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Countries.Web.Services
{
    public class MetadataService : IMetadataService
    {
        private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
        private readonly IServiceCollection _serviceCollection;

        public MetadataService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
            IServiceCollection serviceCollection)
        {
            _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
            _serviceCollection = serviceCollection;
        }

        public DIMetadata GetDependencyInjectionProblems(IServiceProvider serviceProvider)
        {
            var result = new DIMetadata();

            foreach (var service in _serviceCollection)
            {
                var serviceType = service.ServiceType as System.Type;
                try
                {
                    if (serviceType.ContainsGenericParameters)
                    {
                        result.NotValidatedTypes.Add(new ServiceMetadata { ServiceType = serviceType.ToString(), Reason = "Type ContainsGenericParameters == true" });
                        continue;
                    }
                    var x = serviceProvider.GetService(service.ServiceType);
                    result.ValidTypes.Add(serviceType.ToString());
                }
                catch (Exception e)
                {
                    result.Problems.Add(new ServiceMetadata { ServiceType = serviceType.ToString(), Reason = e.Message });
                }
            }

            return result;
        }
    }
}

Muista rekisteröidä MetadataService Startup -luokan ConfigureServices -methodissa:

services.AddSingleton<IMetadataService, MetadataService>();

Sinun täytyy rekisteröidä myös IServiceColletion Singleton -instanssi Startup -luokan ConfigureServices -methodissa:

services.AddSingleton(services);

Loin tarkoituksella yhden DI -ongelman (Injektoin ICountriesService luokaan CountriesInMemoryRepository), alla esimerkkivastaus kun kutsuin esimerkkisovelluksen /api/metadata/dependencyinjection endpointtia (poistin suurimman osan validTypes -arvoista jotta tulos olisi lyhyempi):

{
    "problems": [
        {
            "serviceType": "Countries.Web.Contracts.ICountriesInMemoryRepository",
            "reason": "A circular dependency was detected for the service of type 'Countries.Web.Contracts.ICountriesInMemoryRepository'.\r\nCountries.Web.Contracts.ICountriesInMemoryRepository(Countries.Web.Repositories.CountriesInMemoryRepository) -> Countries.Web.Contracts.ICountriesService(Countries.Web.Services.CountriesService) -> Countries.Web.Contracts.ICountriesInMemoryRepository"
        },
        {
            "serviceType": "Countries.Web.Contracts.ICountriesService",
            "reason": "A circular dependency was detected for the service of type 'Countries.Web.Contracts.ICountriesService'.\r\nCountries.Web.Contracts.ICountriesService(Countries.Web.Services.CountriesService) -> Countries.Web.Contracts.ICountriesInMemoryRepository(Countries.Web.Repositories.CountriesInMemoryRepository) -> Countries.Web.Contracts.ICountriesService"
        }
    ],
    "notValidatedTypes": [
        {
            "serviceType": "Microsoft.Extensions.Options.IOptions`1[TOptions]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Options.IOptionsSnapshot`1[TOptions]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Options.IOptionsMonitor`1[TOptions]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Options.IOptionsFactory`1[TOptions]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Options.IOptionsMonitorCache`1[TOptions]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Logging.ILogger`1[TCategoryName]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Logging.Configuration.ILoggerProviderConfiguration`1[T]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper`1[TModel]",
            "reason": "Type ContainsGenericParameters == true"
        },
        {
            "serviceType": "Microsoft.Extensions.Http.ITypedHttpClientFactory`1[TClient]",
            "reason": "Type ContainsGenericParameters == true"
        }
    ],
    "validTypes": [
        "Microsoft.AspNetCore.Mvc.Cors.CorsAuthorizationFilter",
        "Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.AspNetCore.Mvc.Infrastructure.MvcCompatibilityOptions]",
        "Microsoft.Extensions.Http.HttpMessageHandlerBuilder",
        "System.Net.Http.IHttpClientFactory",
        "Microsoft.Extensions.Http.IHttpMessageHandlerBuilderFilter",
        "Countries.Web.Contracts.IRestCountriesService",
        "Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.Extensions.Http.HttpClientFactoryOptions]",
        "Microsoft.Extensions.Options.IConfigureOptions`1[Microsoft.Extensions.Http.HttpClientFactoryOptions]",
        "Countries.Web.Contracts.IMetadataService",
        "Microsoft.Extensions.DependencyInjection.IServiceCollection"
    ]
}

Seuraavaksi yllä olevaa metadataa käytetään integraatiotestissä. Jos Problems -listalla on yksikin item, testi epäonnistuu.

INTERGRAATIOTESTIN RAKENTAMINEN

Käytän XUnit test frameworkina.

Tärkeää: Älä käytä testispesifistä startup -luokkaa jota käytettiin alkuperäisessä artikkelissa, on tärkeää käyttää sovelluksen oikeaa startup -luokkaa jotta kaikki oikeat servicet injektoidaan samalla lailla kuin sovellus pyörisi oikeassa ympäristössä.

CONTROLLERTESTIEN KANTALUOKKA

Laajensin alkuperäistä kantaluokkaa ReadDependencyInjectionMetadataAsync() funktiolla. Funktion kutsuu /api/metadata/dependencyinjection endpointia:

protected async Task<DIMetadata> ReadDependencyInjectionMetadataAsync()
{
    using (var msg = new HttpRequestMessage(HttpMethod.Get, BuildUri("Metadata", parameters: "/dependencyinjection").ToString()))
    {
        using (var response = await Client.SendAsync(msg).ConfigureAwait(false))
        {
            Assert.Equal(HttpStatusCode.OK, response.StatusCode);

            if (!response.IsSuccessStatusCode)
            {
                return null;
            }

            ValidateHeaders(response.Headers);

            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
            if (string.IsNullOrEmpty(content))
            {
                return null;
            }

            return JsonConvert.DeserializeObject<DIMetadata>(content);
        }
    }
}

TESTILUOKKA

Testiluokka ei tee paljon, se

  • saa dependency injection metadatan käyttäen kantaluokkaa
  • tarkistaa ettei problems -listalla ole yhtään itemiä

Tässä testiluokan toteutus:

using Xunit;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Testing;

namespace Countries.Tests.ControllerTests
{
    public class DependencyInjectionTests : ControllerTestBase, IClassFixture<WebApplicationFactory<Web.Startup>>
    {
        public DependencyInjectionTests(WebApplicationFactory<Web.Startup> factory)
            : base(factory.CreateClient())
        {
        }

        [Fact]
        public async Task Test()
        {
            var diMetadata = await ReadDependencyInjectionMetadataAsync().ConfigureAwait(false);

            Assert.NotNull(diMetadata);
            Assert.True(!diMetadata.Problems.Any());
        }
    }
}

Kun testi suoritetaan, se onnistuu koska dependency injection konfiguraatio on oikein esimerkkisovelluksessa.