.Net Core Unit Testing - Mock IOptions <T>

135

Ich habe das Gefühl, dass mir hier etwas wirklich Offensichtliches fehlt. Ich habe Klassen, in denen Optionen mithilfe des .Net Core IOptions-Musters (?) Eingefügt werden müssen. Wenn ich zum Unit-Test dieser Klasse gehe, möchte ich verschiedene Versionen der Optionen verspotten, um die Funktionalität der Klasse zu überprüfen. Weiß jemand, wie man IOptions außerhalb der Startup-Klasse korrekt verspottet / instanziiert / auffüllt?

Hier sind einige Beispiele der Klassen, mit denen ich arbeite:

Einstellungs- / Optionsmodell

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OptionsSample.Models
{
    public class SampleOptions
    {
        public string FirstSetting { get; set; }
        public int SecondSetting { get; set; }
    }
}

Zu testende Klasse, die die Einstellungen verwendet:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OptionsSample.Models
using System.Net.Http;
using Microsoft.Extensions.Options;
using System.IO;
using Microsoft.AspNetCore.Http;
using System.Xml.Linq;
using Newtonsoft.Json;
using System.Dynamic;
using Microsoft.Extensions.Logging;

namespace OptionsSample.Repositories
{
    public class SampleRepo : ISampleRepo
    {
        private SampleOptions _options;
        private ILogger<AzureStorageQueuePassthru> _logger;

        public SampleRepo(IOptions<SampleOptions> options)
        {
            _options = options.Value;
        }

        public async Task Get()
        {
        }
    }
}

Unit Test in einer anderen Baugruppe als die anderen Klassen:

using OptionsSample.Repositories;
using OptionsSample.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

namespace OptionsSample.Repositories.Tests
{
    public class SampleRepoTests
    {
        private IOptions<SampleOptions> _options;
        private SampleRepo _sampleRepo;


        public SampleRepoTests()
        {
            //Not sure how to populate IOptions<SampleOptions> here
            _options = options;

            _sampleRepo = new SampleRepo(_options);
        }
    }
}
Matt
quelle
1
Könnten Sie ein kleines Codebeispiel für den Block bereitstellen, den Sie verspotten möchten? Vielen Dank!
AJ X.
Verwechseln Sie die Bedeutung von Spott? Sie verspotten eine Schnittstelle und konfigurieren sie so, dass sie einen bestimmten Wert zurückgibt. Denn IOptions<T>Sie müssen sich nur verspotten Value, um die gewünschte Klasse zurückzugeben
Tseng

Antworten:

252

Sie müssen ein IOptions<SampleOptions>Objekt manuell erstellen und füllen . Sie können dies über die Microsoft.Extensions.Options.OptionsHilfsklasse tun . Beispielsweise:

IOptions<SampleOptions> someOptions = Options.Create<SampleOptions>(new SampleOptions());

Sie können das ein wenig vereinfachen, um:

var someOptions = Options.Create(new SampleOptions());

Offensichtlich ist dies nicht sehr nützlich. Sie müssen tatsächlich ein SampleOptions-Objekt erstellen, füllen und an die Create-Methode übergeben.

Necoras
quelle
Ich schätze all die zusätzlichen Antworten, die zeigen, wie man Moq usw. verwendet, aber diese Antwort ist so einfach, dass es definitiv die ist, die ich verwende. Und es funktioniert super!
Grahamesd
Gute Antwort. Weitaus einfacher als sich auf ein spöttisches Framework zu verlassen.
Chris Lawrence
2
Vielen Dank. Ich hatte es so satt, new OptionsWrapper<SampleOptions>(new SampleOptions());überall zu schreiben
BritishDeveloper
59

Wenn Sie das Mocking Framework verwenden möchten, wie im Kommentar unter @TSeng angegeben, müssen Sie der Datei project.json die folgende Abhängigkeit hinzufügen.

   "Moq": "4.6.38-alpha",

Sobald die Abhängigkeit wiederhergestellt ist, ist die Verwendung des MOQ-Frameworks so einfach wie das Erstellen einer Instanz der SampleOptions-Klasse und das Zuweisen dieser Instanz zum Wert.

Hier ist eine Code-Übersicht, wie es aussehen würde.

SampleOptions app = new SampleOptions(){Title="New Website Title Mocked"}; // Sample property
// Make sure you include using Moq;
var mock = new Mock<IOptions<SampleOptions>>();
// We need to set the Value of IOptions to be the SampleOptions Class
mock.Setup(ap => ap.Value).Returns(app);

Sobald der Mock eingerichtet ist, können Sie das Mock-Objekt als an den Konstruktor übergeben

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Zu Ihrer Information Ich habe ein Git-Repository, das diese beiden Ansätze auf Github / patvin80 beschreibt

patvin80
quelle
Dies sollte die akzeptierte Antwort sein, es funktioniert perfekt.
Alessandrocb
Ich wünschte wirklich, das hätte bei mir funktioniert, aber es funktioniert nicht :( Moq 4.13.1
kanpeki
21

Sie können die Verwendung von MOQ überhaupt vermeiden. Verwenden Sie in Ihren Tests die .json-Konfigurationsdatei. Eine Datei für viele Testklassendateien. ConfigurationBuilderIn diesem Fall ist die Verwendung in Ordnung .

Beispiel für appsetting.json

{
    "someService" {
        "someProp": "someValue
    }
}

Beispiel für eine Einstellungszuordnungsklasse:

public class SomeServiceConfiguration
{
     public string SomeProp { get; set; }
}

Beispiel für einen Service, der zum Testen benötigt wird:

public class SomeService
{
    public SomeService(IOptions<SomeServiceConfiguration> config)
    {
        _config = config ?? throw new ArgumentNullException(nameof(_config));
    }
}

NUnit-Testklasse:

[TestFixture]
public class SomeServiceTests
{

    private IOptions<SomeServiceConfiguration> _config;
    private SomeService _service;

    [OneTimeSetUp]
    public void GlobalPrepare()
    {
         var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", false)
            .Build();

        _config = Options.Create(configuration.GetSection("someService").Get<SomeServiceConfiguration>());
    }

    [SetUp]
    public void PerTestPrepare()
    {
        _service = new SomeService(_config);
    }
}
aleha
quelle
Das hat gut für mich funktioniert, Prost! Ich wollte Moq nicht für etwas verwenden, das so einfach schien, und wollte nicht versuchen, meine eigenen Optionen mit Konfigurationseinstellungen zu füllen.
Harry
3
Funktioniert hervorragend, aber die wichtige fehlende Information ist, dass Sie das Microsoft.Extensions.Configuration.Binder-Nuget-Paket einschließen müssen, da sonst die Erweiterungsmethode "Get <SomeServiceConfiguration>" nicht verfügbar ist.
Kinetic
Ich musste das dotnet add-Paket Microsoft.Extensions.Configuration.Json ausführen, damit dies funktioniert. Gute Antwort!
Leonardo Wildt
1
Ich musste auch die Eigenschaften der Datei appsettings.json ändern, um die Datei in der bin-Datei zu verwenden, da Directory.GetCurrentDirectory () den Inhalt der bin-Datei zurückgab. In "In Ausgabeverzeichnis kopieren" von appsettings.json setze ich den Wert auf "Kopieren, wenn neuer".
bpz
14

Gegebene Klasse Person, die PersonSettingswie folgt abhängt :

public class PersonSettings
{
    public string Name;
}

public class Person
{
    PersonSettings _settings;

    public Person(IOptions<PersonSettings> settings)
    {
        _settings = settings.Value;
    }

    public string Name => _settings.Name;
}

IOptions<PersonSettings> kann verspottet werden und Person wie folgt getestet werden:

[TestFixture]
public class Test
{
    ServiceProvider _provider;

    [OneTimeSetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        // mock PersonSettings
        services.AddTransient<IOptions<PersonSettings>>(
            provider => Options.Create<PersonSettings>(new PersonSettings
            {
                Name = "Matt"
            }));
        _provider = services.BuildServiceProvider();
    }

    [Test]
    public void TestName()
    {
        IOptions<PersonSettings> options = _provider.GetService<IOptions<PersonSettings>>();
        Assert.IsNotNull(options, "options could not be created");

        Person person = new Person(options);
        Assert.IsTrue(person.Name == "Matt", "person is not Matt");    
    }
}

Zu injizieren IOptions<PersonSettings>in Personanstatt sie explizit auf die Ctor geben, verwenden Sie diesen Code:

[TestFixture]
public class Test
{
    ServiceProvider _provider;

    [OneTimeSetUp]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddTransient<IOptions<PersonSettings>>(
            provider => Options.Create<PersonSettings>(new PersonSettings
            {
                Name = "Matt"
            }));
        services.AddTransient<Person>();
        _provider = services.BuildServiceProvider();
    }

    [Test]
    public void TestName()
    {
        Person person = _provider.GetService<Person>();
        Assert.IsNotNull(person, "person could not be created");

        Assert.IsTrue(person.Name == "Matt", "person is not Matt");
    }
}
Frank Rem
quelle
Sie testen nichts Nützliches. Das Framework für DI my Microsoft ist bereits Unit-getestet. Derzeit ist dies wirklich ein Integrationstest (Integration in ein Framework eines Drittanbieters).
Erik Philips
2
@ErikPhilips Mein Code zeigt, wie IOptions <T> verspottet werden, wie vom OP angefordert. Ich bin damit einverstanden, dass es nichts Nützliches an sich testet, aber es kann nützlich sein, etwas anderes zu testen.
Frank Rem
13

Sie können Ihre Optionen jederzeit über Options.Create () erstellen und dann einfach AutoMocker.Use (Optionen) verwenden, bevor Sie die verspottete Instanz des zu testenden Repositorys erstellen. Mit AutoMocker.CreateInstance <> () können Instanzen einfacher erstellt werden, ohne dass Parameter manuell übergeben werden müssen

Ich habe geändert, dass Sie SampleRepo ein wenig sind, um das Verhalten reproduzieren zu können, von dem ich denke, dass Sie es erreichen möchten.

public class SampleRepoTests
{
    private readonly AutoMocker _mocker = new AutoMocker();
    private readonly ISampleRepo _sampleRepo;

    private readonly IOptions<SampleOptions> _options = Options.Create(new SampleOptions()
        {FirstSetting = "firstSetting"});

    public SampleRepoTests()
    {
        _mocker.Use(_options);
        _sampleRepo = _mocker.CreateInstance<SampleRepo>();
    }

    [Fact]
    public void Test_Options_Injected()
    {
        var firstSetting = _sampleRepo.GetFirstSetting();
        Assert.True(firstSetting == "firstSetting");
    }
}

public class SampleRepo : ISampleRepo
{
    private SampleOptions _options;

    public SampleRepo(IOptions<SampleOptions> options)
    {
        _options = options.Value;
    }

    public string GetFirstSetting()
    {
        return _options.FirstSetting;
    }
}

public interface ISampleRepo
{
    string GetFirstSetting();
}

public class SampleOptions
{
    public string FirstSetting { get; set; }
}
matei
quelle
8

Hier ist ein weiterer einfacher Weg, der kein Mock benötigt, sondern den OptionsWrapper verwendet:

var myAppSettingsOptions = new MyAppSettingsOptions();
appSettingsOptions.MyObjects = new MyObject[]{new MyObject(){MyProp1 = "one", MyProp2 = "two", }};
var optionsWrapper = new OptionsWrapper<MyAppSettingsOptions>(myAppSettingsOptions );
var myClassToTest = new MyClassToTest(optionsWrapper);
Robert Corvus
quelle
2

Für meine System- und Integrationstests bevorzuge ich eine Kopie / einen Link meiner Konfigurationsdatei im Testprojekt. Und dann benutze ich den ConfigurationBuilder, um die Optionen zu erhalten.

using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace SomeProject.Test
{
public static class TestEnvironment
{
    private static object configLock = new object();

    public static ServiceProvider ServiceProvider { get; private set; }
    public static T GetOption<T>()
    {
        lock (configLock)
        {
            if (ServiceProvider != null) return (T)ServiceProvider.GetServices(typeof(T)).First();

            var builder = new ConfigurationBuilder()
                .AddJsonFile("config/appsettings.json", optional: false, reloadOnChange: true)
                .AddEnvironmentVariables();
            var configuration = builder.Build();
            var services = new ServiceCollection();
            services.AddOptions();

            services.Configure<ProductOptions>(configuration.GetSection("Products"));
            services.Configure<MonitoringOptions>(configuration.GetSection("Monitoring"));
            services.Configure<WcfServiceOptions>(configuration.GetSection("Services"));
            ServiceProvider = services.BuildServiceProvider();
            return (T)ServiceProvider.GetServices(typeof(T)).First();
        }
    }
}
}

Auf diese Weise kann ich die Konfiguration überall in meinem TestProject verwenden. Für Unit-Tests bevorzuge ich MOQ wie das beschriebene patvin80.

Mithrandir
quelle
1

Stimmen Sie Aleha zu, dass die Verwendung einer Konfigurationsdatei testSettings.json wahrscheinlich besser ist. Und anstatt die IOption zu injizieren, können Sie einfach die echten SampleOptions in Ihren Klassenkonstruktor injizieren. Wenn Sie die Klasse einem Komponententest unterziehen, können Sie Folgendes in einem Fixture oder erneut nur im Testklassenkonstruktor tun:

   var builder = new ConfigurationBuilder()
  .AddJsonFile("testSettings.json", true, true)
  .AddEnvironmentVariables();

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
BobTheOtherBuilder
quelle