.Net Çekirdek Birim Testi - Mock IOptions <T>


137

Burada gerçekten bariz bir şeyi kaçırdığımı hissediyorum. Net Core IOptions modelini (?) Kullanarak seçeneklerin enjekte edilmesini gerektiren sınıflarım var. Bu sınıfın birim testine gittiğimde, sınıfın işlevselliğini doğrulamak için seçeneklerin çeşitli sürümleriyle alay etmek istiyorum. IOptions'ı Startup sınıfının dışında doğru şekilde nasıl alay edeceğini / başlatacağını / dolduracağını bilen var mı?

İşte çalıştığım sınıfların bazı örnekleri:

Ayarlar / Seçenekler Modeli

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

Ayarları kullanan test edilecek sınıf:

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()
        {
        }
    }
}

Diğer sınıflardan farklı bir montajda birim testi:

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

1
Alay etmeye çalıştığınız bloğun küçük bir kod örneğini verebilir misiniz? Teşekkürler!
AJ X.

Alay etmenin anlamını mı karıştırıyorsun? Bir arayüzde alay edersiniz ve belirli bir değeri döndürecek şekilde yapılandırırsınız. İçin IOptions<T>yalnızca alay etmek zorunda Valuearzu sınıfı dönmek için
Tseng

Yanıtlar:


253

Bir IOptions<SampleOptions>nesneyi manuel olarak oluşturmanız ve doldurmanız gerekir . Bunu Microsoft.Extensions.Options.Optionsyardımcı sınıf aracılığıyla yapabilirsiniz . Örneğin:

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

Bunu biraz basitleştirebilirsin:

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

Açıkçası bu, olduğu gibi pek kullanışlı değil. Aslında bir SampleOptions nesnesi oluşturup doldurmanız ve bunu Create yöntemine aktarmanız gerekir.


Moq vb .'nin nasıl kullanılacağını gösteren tüm ek cevapları takdir ediyorum, ancak bu cevap o kadar basit ki kesinlikle benim kullandığım cevap. Ve harika çalışıyor!
grahamesd

Mükemmel cevap. Bir alaycı çerçeveye güvenmekten çok daha basit.
Chris Lawrence

2
Teşekkürler. Her new OptionsWrapper<SampleOptions>(new SampleOptions());yerde yazmaktan çok yoruldum
BritishDeveloper

59

Mocking Framework'ü yorumda @TSeng tarafından belirtildiği gibi kullanmayı planlıyorsanız, project.json dosyanıza aşağıdaki bağımlılığı eklemeniz gerekir.

   "Moq": "4.6.38-alpha",

Bağımlılık geri yüklendikten sonra, MOQ çerçevesini kullanmak, SampleOptions sınıfının bir örneğini oluşturmak ve daha sonra belirtildiği gibi bunu Değere atamak kadar basittir.

İşte nasıl görüneceğine dair bir kod özeti.

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

Model kurulduktan sonra, sahte nesneyi müteahhide şu şekilde iletebilirsiniz:

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

Bilginize Github / patvin80'de bu 2 yaklaşımı özetleyen bir git deposuna sahibim


Kabul edilen cevap bu olmalı, mükemmel çalışıyor.
alessandrocb

Bunun benim için çalışmasını gerçekten isterdim, ancak işe yaramaz :( Moq 4.13.1
kanpeki

21

MOQ'u kullanmaktan hiç kaçınabilirsiniz. Testlerinizin .json yapılandırma dosyasında kullanın. Birçok test sınıfı dosyası için bir dosya. ConfigurationBuilderBu durumda kullanmak iyi olacaktır .

Appsetting.json örneği

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

Sınıf eşleme ayarları örneği:

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

Test edilmesi gereken hizmet örneği:

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

NUnit test sınıfı:

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

Bu benim için güzel çalıştı, şerefe! Moq'u çok basit görünen bir şey için kullanmak istemedim ve kendi seçeneklerimi yapılandırma ayarlarıyla doldurmayı denemek istemedim.
Harry

3
Harika çalışıyor, ancak önemli eksik bilgi parçası Microsoft.Extensions.Configuration.Binder nuget paketini eklemeniz gerektiğidir, aksi takdirde "Get <SomeServiceConfiguration>" uzantı yöntemini kullanamazsınız.
Kinetic

Bunu çalıştırmak için dotnet add package Microsoft.Extensions.Configuration.Json çalıştırmam gerekiyordu. Mükemmel cevap!
Leonardo Wildt

1
Directory.GetCurrentDirectory (), bin dosyasının içeriğini döndürdüğü için, bin dosyasındaki dosyayı kullanmak için appsettings.json dosyasının özelliklerini de değiştirmem gerekiyordu. Appsettings.json dosyasının "Çıktı dizinine kopyala" bölümünde, değeri "daha yeniyse kopyala" olarak ayarladım.
bpz

14

Aşağıdakilere Personbağlı PersonSettingsolarak verilen sınıf :

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>alay Personedilebilir ve aşağıdaki gibi test edilebilir:

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

Enjekte etmek IOptions<PersonSettings>içine Personyerine ctor açıkça iletmek yerine, bu kodu kullanın:

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

Yararlı bir şeyi test etmiyorsun. DI, Microsoft'umun çerçevesi zaten birim testinden geçmiştir. Hali hazırda olduğu gibi, bu gerçekten bir entegrasyon testidir (3. taraf çerçevesiyle entegrasyon).
Erik Philips

2
@ErikPhilips Kodum, OP tarafından talep edildiği gibi IOptions <T> ile nasıl alay edileceğini gösteriyor. Kendi başına yararlı bir şeyi test etmediğini kabul ediyorum, ancak başka bir şeyi test etmek faydalı olabilir.
Frank Rem

13

Seçeneklerinizi her zaman Options.Create () aracılığıyla oluşturabilir ve ayrıca AutoMocker'ı kullanabilirsiniz. (Options) kullanıp test ettiğiniz deponun alay edilmiş örneğini gerçekten oluşturmadan önce. AutoMocker.CreateInstance <> () kullanmak, parametreleri manuel olarak iletmeden örnek oluşturmayı kolaylaştırır

Başarmak istediğinizi düşündüğüm davranışı yeniden üretebilmek için SampleRepo'nuzu biraz değiştirdim.

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

8

Mock'a ihtiyaç duymayan, bunun yerine OptionsWrapper'ı kullanan başka bir kolay yol:

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

2

Sistem ve entegrasyon testlerim için, yapılandırma dosyamın bir kopyasını / bağlantısını test projesi içinde bulundurmayı tercih ediyorum. Ve sonra seçenekleri almak için ConfigurationBuilder'ı kullanıyorum.

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

Bu şekilde yapılandırmayı TestProject'imin her yerinde kullanabilirim. Birim testleri için, patvin80'in tarif ettiği gibi MOQ kullanmayı tercih ediyorum.


1

Aleha ile testSettings.json yapılandırma dosyası kullanmanın muhtemelen daha iyi olacağı konusunda hemfikir olun. Ve sonra IOption'ı enjekte etmek yerine, sınıf kurucunuza gerçek SampleOptions'ı basitçe enjekte edebilirsiniz, sınıfı birim test ederken, aşağıdakileri bir fikstürde veya yine sadece test sınıfı yapıcısında yapabilirsiniz:

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

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.