.Net 핵심 단위 테스트-Mock IOptions <T>


137

나는 여기에 분명한 것이 빠져있는 것처럼 느낍니다. .Net Core IOptions 패턴 (?)을 사용하여 옵션을 주입 해야하는 클래스가 있습니다. 해당 클래스를 단위 테스트 할 때 클래스의 기능을 검증하는 옵션의 다양한 버전을 조롱하고 싶습니다. 누구든지 Startup 클래스 외부에서 IOptions를 올바르게 조롱 / 인스턴스화 / 채우기하는 방법을 알고 있습니까?

다음은 작업중 인 클래스 샘플입니다.

설정 / 옵션 모델

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

설정을 사용하는 테스트 할 클래스 :

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

다른 클래스와 다른 어셈블리의 단위 테스트 :

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
조롱하려는 블록의 작은 코드 예제를 제공 할 수 있습니까? 감사!
AJ X.

조롱의 의미를 혼동하고 있습니까? 인터페이스를 조롱하고 지정된 값을 반환하도록 구성합니다. 들어 IOptions<T>당신은 단지 조롱해야 Value당신이 원하는 클래스 반환

답변:


253

IOptions<SampleOptions>객체 를 수동으로 생성하고 채워야 합니다. Microsoft.Extensions.Options.Options도우미 클래스 를 통해 그렇게 할 수 있습니다 . 예를 들면 다음과 같습니다.

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

다음과 같이 조금 단순화 할 수 있습니다.

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

분명히 이것은 그다지 유용하지 않습니다. 실제로 SampleOptions 객체를 생성하고 채워서 Create 메서드에 전달해야합니다.


Moq 등을 사용하는 방법을 보여주는 모든 추가 답변에 감사하지만이 답변은 너무 간단하여 확실히 내가 사용하는 답변입니다. 그리고 그것은 잘 작동합니다!
grahamesd

좋은 대답입니다. 조롱 프레임 워크에 의존하는 것이 훨씬 간단합니다.
크리스 로렌스

2
감사. 나는 그렇게 쓰는 피곤했다 new OptionsWrapper<SampleOptions>(new SampleOptions());사방
BritishDeveloper

59

주석에 @TSeng으로 표시된 Mocking Framework를 사용하려는 경우 project.json 파일에 다음 종속성을 추가해야합니다.

   "Moq": "4.6.38-alpha",

일단 종속성이 복원되면, SampleOptions 클래스의 인스턴스를 작성하고 언급 한대로 Value에 지정하는 것처럼 MOQ 프레임 워크를 사용하는 것이 간단합니다.

코드 개요는 다음과 같습니다.

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

모의 설정이 완료되면 다음과 같이 모의 객체를 생성자에게 전달할 수 있습니다.

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

참고로 Github / patvin80 에서이 두 가지 접근 방식을 간략하게 설명하는 git 저장소가 있습니다.


이것은 받아 들일만한 대답이어야하며 완벽하게 작동합니다.
alessandrocb

정말이 나를 위해 일한 좋겠지 만, 그렇지 :( MOQ 4.13.1 않습니다
kanpeki

21

MOQ를 전혀 사용하지 않아도됩니다. 테스트 .json 구성 파일에서 사용하십시오. 많은 테스트 클래스 파일을위한 하나의 파일. ConfigurationBuilder이 경우 에 사용 하는 것이 좋습니다.

appsetting.json의 예

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

설정 매핑 클래스의 예 :

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

테스트에 필요한 서비스 예 :

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

NUnit 테스트 클래스 :

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

이것은 나를 위해 잘 작동했습니다, 건배! 너무 단순 해 보이는 구성에 Moq를 사용하고 싶지 않았고 구성 설정으로 내 옵션을 채우고 싶지 않았습니다.
Harry

3
훌륭하게 작동하지만 중요한 누락 정보는 Microsoft.Extensions.Configuration.Binder 너겟 패키지를 포함해야한다는 것입니다. 그렇지 않으면 "Get <SomeServiceConfiguration>"확장 메소드를 사용할 수 없습니다.
Kinetic

dotnet add 패키지 Microsoft.Extensions.Configuration.Json을 실행 하여이 작업을 수행해야했습니다. 좋은 답변입니다!
Leonardo Wildt

1
Directory.GetCurrentDirectory ()가 bin 파일의 내용을 반환하므로 bin 파일의 파일을 사용하도록 appsettings.json 파일의 속성을 변경해야했습니다. appsettings.json의 "출력 디렉토리로 복사"에서 값을 "최신 경우 복사"로 설정했습니다.
bpz

14

다음과 같은 클래스 Person가 제공 PersonSettings됩니다.

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>조롱하고 Person다음과 같이 테스트 할 수 있습니다.

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

주입 IOptions<PersonSettings>Person대신의 ctor에 명시 적으로 전달,이 코드를 사용 :

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

유용한 것을 테스트하지 않습니다. DI my Microsoft의 프레임 워크는 이미 단위 테스트를 거쳤습니다. 실제로 이것은 통합 테스트 (타사 프레임 워크와의 통합)입니다.
Erik Philips

2
@ErikPhilips 내 코드는 OP의 요청에 따라 IOptions <T>를 조롱하는 방법을 보여줍니다. 나는 그 자체로 유용한 것을 테스트하지는 않지만 다른 것을 테스트하는 것이 유용 할 수 있다는 데 동의합니다.
Frank Rem

13

테스트중인 리포지토리의 모의 인스턴스를 실제로 만들기 전에 Options.Create ()를 통해 옵션을 만들고 AutoMocker.Use (options)를 사용하는 것보다 언제든지 옵션을 만들 수 있습니다. AutoMocker.CreateInstance <> ()를 사용하면 매개 변수를 수동으로 전달하지 않고도 인스턴스를 쉽게 만들 수 있습니다.

달성하고자하는 동작을 재현 할 수 있도록 SampleRepo를 약간 변경했습니다.

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을 필요로하지 않고 대신 OptionsWrapper를 사용하는 또 다른 쉬운 방법이 있습니다.

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

내 시스템 및 통합 테스트의 경우 테스트 프로젝트 내에 구성 파일의 복사 / 링크를 선호합니다. 그런 다음 ConfigurationBuilder를 사용하여 옵션을 가져옵니다.

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

이 방법으로 TestProject 내 어디에서나 구성을 사용할 수 있습니다. 단위 테스트의 경우 patvin80과 같은 MOQ를 사용하는 것이 좋습니다.


1

Aleha와 함께 testSettings.json 구성 파일을 사용하는 것이 더 좋습니다. 그런 다음 IOption을 주입하는 대신 클래스 생성자에 실제 SampleOptions를 주입하면됩니다. 단위를 테스트 할 때 조명기에서 또는 테스트 클래스 생성자에서 다음을 수행 할 수 있습니다.

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

  var configurationRoot = builder.Build();
  configurationRoot.GetSection("SampleRepo").Bind(_sampleRepo);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.