Как выполнить модульный тест, вызывающий расширение IConfiguration.Get<T>

У меня есть очень простой метод, который мне нужен для модульного тестирования.

public static class ValidationExtensions
{
    public static T GetValid<T>(this IConfiguration configuration)
    {
        var obj = configuration.Get<T>();
        Validator.ValidateObject(obj, new ValidationContext(obj), true);
        return obj;
    }
}

Проблема в том, что configuration.Get<T> является методом статического расширения и не принадлежит IConfiguration, Я не могу изменить реализацию этого статического метода.

Я думаю, возможно, самый простой способ - создать провайдера конфигурации памяти? Но я не знаю, смогу ли я создать его, не привязав его к веб-хостингу.

4 ответа

Решение

Модуль конфигурации не зависит от функций, связанных с веб-хостингом.

Вы должны быть в состоянии создать конфигурацию в памяти для тестирования без необходимости привязывать ее к веб-хосту.

Просмотрите следующий пример теста

public class TestConfig {
    [Required]
    public string SomeKey { get; set; }
    [Required] //<--NOTE THIS
    public string SomeOtherKey { get; set; }
}

//...

[Fact]
public void Should_Fail_Validation_For_Required_Key() {
    //Arrange
    var inMemorySettings = new Dictionary<string, string>
    {
        {"Email:SomeKey", "value1"},
        //{"Email:SomeOtherKey", "value2"}, //Purposely omitted for required failure
        //...populate as needed for the test
    };

    IConfiguration configuration = new ConfigurationBuilder()
        .AddInMemoryCollection(inMemorySettings)
        .Build();

    //Act
    Action act = () => configuration.GetSection("Email").GetValid<TestConfig>();

    //Assert
    ValidationException exception = Assert.Throws<ValidationException>(act);
    //...other assertions of validation results within exception object
}

По моему мнению, это будет близко к интеграционному тесту, но в идеале вы просто используете функции, зависящие от фреймворка, чтобы изолировать тестирование метода расширения.

Немного другой способ решения проблемы, избегая Mock и большого количества шума установки:

InMemoryConfiguration почти дала мне то, что мне было нужно, поэтому я расширил его, чтобы вы могли изменять значения после создания конфигурации (ситуация, в которой я был, я не знал всех фиктивных значений во время создания конфигурации)

https://gist.github.com/martinsmith1968/9567de76d2bbe537af05d76eb39b1162

Модульный тест внизу показывает использование

Большинство имитационных библиотек (Moq, FakeItEasy и т. д.) не могут имитировать методы расширения.

Таким образом, вы должны «заполнить» свою IConfiguration таким образом, чтобы возвращаемый экземпляр ответа T. Nkoski работал во многих сценариях, но не в том случае, если вам нужно протестировать код, который вызывает IConfiguration.Get<T>вы можете использовать пример ниже:

      using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Xunit;

public class TestClass {
    public class Movie
    {
        public string Name { get; set; }
        public decimal Rating { get; set; }
        public IList<string> Stars { get; set; } //it works with collections
    }

    [Fact]
    public void MyTest()
    {
        var movie = new Movie { 
            Name = "Some Movie",
            Rating = 9, 
            Stars = new List<string>{"Some actress", "Some actor"}
        };

        var movieAsJson = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(movie));
        using(var stream = new MemoryStream(movieAsJson))
        {
            var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
            var movieFromConfig = config.Get<Movie>();
            //var sut = new SomeService(config).SomeMethodThatCallsConfig.Get<Movie>()
        }
    }
}
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        IConfiguration mock = new MockConfiguration();
        var simpleObject = mock.GetValid<SimpleObject>();
        Assert.AreEqual(simpleObject.MyConfigStr, "123");
    }
}

public class SimpleObject
{
    public string MyConfigStr { get; set; }
}


public class MockConfiguration : IConfiguration
{
    public IConfigurationSection GetSection(string key)
    {
        return new MockConfigurationSection()
        {
            Value = "123"
        };
    }

    public IEnumerable<IConfigurationSection> GetChildren()
    {
        var configurationSections = new List<IConfigurationSection>()
        {
            new MockConfigurationSection()
            {
                Value = "MyConfigStr"
            }
        };
        return configurationSections;
    }

    public Microsoft.Extensions.Primitives.IChangeToken GetReloadToken()
    {
        throw new System.NotImplementedException();
    }

    public string this[string key]
    {
        get => throw new System.NotImplementedException();
        set => throw new System.NotImplementedException();
    }
}

public class MockConfigurationSection : IConfigurationSection
{
    public IConfigurationSection GetSection(string key)
    {
        return this;
    }

    public IEnumerable<IConfigurationSection> GetChildren()
    {
        return new List<IConfigurationSection>();
    }

    public IChangeToken GetReloadToken()
    {
        return new MockChangeToken();
    }

    public string this[string key]
    {
        get => throw new System.NotImplementedException();
        set => throw new System.NotImplementedException();
    }

    public string Key { get; }
    public string Path { get; }
    public string Value { get; set; }
}

public class MockChangeToken : IChangeToken
{
    public IDisposable RegisterChangeCallback(Action<object> callback, object state)
    {
        return new MockDisposable();
    }

    public bool HasChanged { get; }
    public bool ActiveChangeCallbacks { get; }
}

public class MockDisposable : IDisposable
{
    public void Dispose()
    {
    }
}

создал макет для IConfiguration и имитировать поведение ConfigBinder

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

добавил эти два пространства имён для компиляции

Другие вопросы по тегам