.Net Core Unit Testing - Mock IOptions <T>

137

Saya merasa seperti kehilangan sesuatu yang sangat jelas di sini. Saya memiliki kelas yang memerlukan injeksi opsi menggunakan pola Net. IOptions (?). Ketika saya pergi ke unit test kelas itu, saya ingin mengejek berbagai versi opsi untuk memvalidasi fungsionalitas kelas. Adakah yang tahu bagaimana cara mengejek / instantiate / mengisi IOptions dengan benar di luar kelas Startup?

Berikut adalah beberapa contoh kelas yang saya kerjakan:

Pengaturan / Opsi Model

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

Kelas yang akan diuji yang menggunakan Pengaturan:

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

Tes unit dalam majelis berbeda dari kelas lain:

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);
        }
    }
}
Mat
sumber
1
Bisakah Anda memberikan contoh kode kecil dari blok yang Anda coba tiru? Terima kasih!
AJ X.
Apakah Anda bingung arti mengejek? Anda mengejek antarmuka dan mengonfigurasinya untuk mengembalikan nilai yang ditentukan. Karena IOptions<T>kamu hanya perlu mengejek Valueuntuk mengembalikan kelas yang kamu inginkan
Tseng

Jawaban:

253

Anda perlu membuat dan mengisi IOptions<SampleOptions>objek secara manual . Anda dapat melakukannya melalui Microsoft.Extensions.Options.Optionskelas pembantu. Sebagai contoh:

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

Anda dapat menyederhanakannya menjadi:

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

Jelas ini tidak terlalu berguna. Anda harus benar-benar membuat dan mengisi objek SampleOptions dan meneruskannya ke metode Buat.

Necoras
sumber
Saya menghargai semua jawaban tambahan yang menunjukkan cara menggunakan Moq, dll., Tetapi jawaban ini sangat sederhana sehingga pasti yang saya gunakan. Dan itu berhasil!
grahamesd
Jawaban yang bagus Jauh lebih sederhana yang mengandalkan kerangka mengejek.
Chris Lawrence
2
Terima kasih. Saya sangat lelah menulis di new OptionsWrapper<SampleOptions>(new SampleOptions());mana
Pengembang Inggris
59

Jika Anda bermaksud untuk menggunakan Mocking Framework seperti yang ditunjukkan oleh @TSeng dalam komentar, Anda perlu menambahkan ketergantungan berikut di file project.json Anda.

   "Moq": "4.6.38-alpha",

Setelah dependensi dipulihkan, menggunakan kerangka kerja MOQ semudah membuat instance kelas SampleOptions dan kemudian seperti yang disebutkan, tetapkan nilai tersebut ke Value.

Berikut ini adalah garis besar kode yang akan terlihat.

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

Setelah tiruan diatur, Anda sekarang dapat meneruskan objek tiruan ke konstruktor sebagai

SampleRepo sr = new SampleRepo(mock.Object);   

HTH.

FYI Saya memiliki repositori git yang menguraikan 2 pendekatan ini pada Github / patvin80

patvin80
sumber
Ini harus menjadi jawaban yang diterima, itu berfungsi dengan baik.
alessandrocb
Benar-benar berharap ini berhasil untuk saya, tetapi tidak :( Moq 4.13.1
kanpeki
21

Anda dapat menghindari penggunaan MOQ sama sekali. Gunakan dalam file konfigurasi .json pengujian Anda. Satu file untuk banyak file kelas uji. Akan baik-baik saja untuk digunakan ConfigurationBuilderdalam kasus ini.

Contoh appsetting.json

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

Contoh kelas pemetaan pengaturan:

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

Contoh layanan yang diperlukan untuk menguji:

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

Kelas tes 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);
    }
}
aleha
sumber
Ini bekerja dengan baik untukku, tepuk tangan! Tidak ingin menggunakan Moq untuk sesuatu yang tampak sangat sederhana dan tidak ingin mencoba mengisi pilihan saya sendiri dengan pengaturan konfigurasi.
Harry
3
Berfungsi bagus, tetapi informasi penting yang hilang adalah Anda harus menyertakan paket nuget Microsoft.Extensions.Configuration.Binder, jika tidak Anda tidak mendapatkan metode ekstensi "Dapatkan <SomeServiceConfiguration>".
Kinetic
Saya harus menjalankan dotnet menambahkan paket Microsoft.Extensions.Configuration.Json untuk mendapatkan ini berfungsi. Jawaban bagus!
Leonardo Wildt
1
Saya juga harus mengubah properti file appsettings.json untuk menggunakan file dalam file bin, seperti Directory.GetCurrentDirectory () mengembalikan konten file bin. Di "Salin ke direktori keluaran" appsettings.json, saya menetapkan nilainya ke "Salin jika lebih baru".
bpz
14

Kelas Personyang diberikan tergantung pada PersonSettingssebagai berikut:

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>dapat diejek dan Persondapat diuji sebagai berikut:

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

Untuk menyuntikkan IOptions<PersonSettings>ke dalam Personbukan lewat secara eksplisit untuk ctor, gunakan kode ini:

[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
sumber
Anda tidak menguji sesuatu yang bermanfaat. Kerangka kerja untuk DI my Microsoft saya sudah diuji unit. Seperti berdiri ini benar-benar tes integrasi (integrasi dengan kerangka kerja pihak ke-3).
Erik Philips
2
@ErikPhilips Kode saya menunjukkan cara mengejek IOptions <T> seperti yang diminta oleh OP. Saya setuju bahwa itu tidak menguji apa pun yang berguna dalam dirinya sendiri, tetapi dapat bermanfaat menguji sesuatu yang lain.
Frank Rem
13

Anda selalu dapat membuat opsi melalui Options.Create () dan daripada hanya menggunakan AutoMocker.Use (options) sebelum benar-benar membuat instance tiruan dari repositori yang Anda uji. Menggunakan AutoMocker.CreateInstance <> () membuatnya lebih mudah untuk membuat instance tanpa melewati parameter secara manual

Saya telah mengubah Anda sedikit SampleRepo agar dapat mereproduksi perilaku yang saya pikir ingin Anda capai.

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
sumber
8

Berikut cara mudah lain yang tidak perlu Mock, tetapi gunakan 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);
Robert Corvus
sumber
2

Untuk pengujian sistem dan integrasi, saya lebih suka memiliki salinan / tautan file konfigurasi saya di dalam proyek pengujian. Dan kemudian saya menggunakan ConfigurationBuilder untuk mendapatkan opsi.

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

Dengan cara ini saya dapat menggunakan konfigurasi di mana saja di dalam TestProject saya. Untuk pengujian unit, saya lebih suka menggunakan MOQ seperti yang dijelaskan oleh patvin80.

Mithrandir
sumber
1

Setuju dengan Aleha yang menggunakan file konfigurasi testSettings.json mungkin lebih baik. Dan alih-alih menyuntikkan IOption Anda cukup menyuntikkan SampleOptions nyata di konstruktor kelas Anda, ketika unit menguji kelas, Anda dapat melakukan hal berikut di fixture atau lagi hanya di konstruktor kelas tes:

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

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