Мокинг CloudStorageAccount и CloudTable для хранилища таблиц Azure

Поэтому я пытаюсь протестировать хранилище таблиц Azure и имитировать вещи, от которых я зависим. Мой класс структурирован таким образом, что я устанавливаю соединение в конструкторе, то есть я создаю новый экземпляр CloudStorageAccount, в котором я создаю экземпляр StorageCredentials с storageName и storageKey. После этого я создаю экземпляр CloudTable, который использую далее в коде для выполнения операций CRUD. Мой класс выглядит следующим образом:

public class AzureTableStorageService : ITableStorage
{
        private const string _records = "myTable";
        private CloudStorageAccount _storageAccount;
        private CloudTable _table;

        public AzureTableStorageService()
        {
            _storageAccount = new CloudStorageAccount(new StorageCredentials(
                 ConfigurationManager.azureTableStorageName, ConfigurationManager.azureTableStorageKey), true);
            _table = _storageAccount.CreateCloudTableClient().GetTableReference(_records);
            _table.CreateIfNotExistsAsync();
        }

        //...
        //Other methods here
}

_table повторно используется в классе для различных целей. Моя цель - издеваться над ним, но поскольку он виртуальный и не реализует никакого интерфейса, я не могу найти простое решение Mock, например:

_storageAccount = new Mock<CloudStorageAccount>(new Mock<StorageCredentials>(("dummy", "dummy"), true));
_table  = new Mock<CloudTable>(_storageAccount.Object.CreateCloudTableClient().GetTableReference(_records));

Поэтому, когда я пытаюсь построить свой модульный тест таким образом, я получаю: Type to mock must be an interface or an abstract or non-sealed class.

Моя цель - добиться чего-то вроде:

_table.Setup(x => x.DoSomething()).ReturnsAsync("My desired result");

Любые идеи приветствуются!

Я смотрю на API, и класс не запечатан, и все методы в этом примере фактически виртуальные. Поэтому я не могу привести пример того, как это исправить, потому что мне кажется, что в этом нет никаких проблем. Я также действительно не понимаю, как вы можете реально смоделировать зависимости в этом примере. Вам действительно следует использовать фабричный объект, чтобы предоставить таблицу и передать ее через конструктор. Таким образом, остается только одна строка кода, и ее гораздо проще заменить в UnitTesting.

Ambidex 28.11.2018 00:57

@Ambidex извините за путаницу, есть идеи по поводу издевательства над ним?

Coke 28.11.2018 01:02

Я отредактировал свой предыдущий комментарий. В основном в этом сценарии я бы предложил использовать фабрику для создания таблицы, а затем передать ее через конструктор класса TableStorage. Все классы в этом примере позволяют имитировать и использовать виртуальные методы. refactoring.guru/design-patterns/factory-method

Ambidex 28.11.2018 01:05

@Ambidex, который кажется прямым подходом, я ищу способы издеваться над ним, как есть, без изменения поведения класса. Мне интересно, возможно ли это. Спасибо за разъяснения, ценю!

Coke 28.11.2018 01:21

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

Nkosi 28.11.2018 01:21

Все эти проблемы, связанные с облаком, следует рассматривать как сторонние внешние зависимости и абстрагироваться.

Nkosi 28.11.2018 01:40

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

Peter Bons 28.11.2018 08:05

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

Ivan Yang 29.11.2018 04:02

@Nkosi, у тебя есть лучший дизайн этого. Я понимаю, когда вы говорите, что он тесно связан, но не могли бы вы привести пример этого со слабой связью?

CredibleAshok 15.07.2021 07:42

@Coke, ты получил свой ответ, если да, то поделись им. заранее спасибо

CredibleAshok 15.07.2021 07:42
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
10
10 720
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

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

/// <summary>
/// Mock class for CloudTable object
/// </summary>
public class MockCloudTable : CloudTable
{

    public MockCloudTable(Uri tableAddress) : base(tableAddress)
    { }

    public MockCloudTable(StorageUri tableAddress, StorageCredentials credentials) : base(tableAddress, credentials)
    { }

    public MockCloudTable(Uri tableAbsoluteUri, StorageCredentials credentials) : base(tableAbsoluteUri, credentials)
    { }

    public async override Task<TableResult> ExecuteAsync(TableOperation operation)
    {
        return await Task.FromResult(new TableResult
        {
            Result = new ScreenSettingEntity() { Settings = "" },
            HttpStatusCode = 200
        });
    }
}

Я создал экземпляр фиктивного класса, передав строку конфигурации, используемую для локального хранилища эмулятором хранилища (см. https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string).

var mockTable = new MockCloudTable(new Uri("http://127.0.0.1:10002/devstoreaccount1/screenSettings"));

В этом примере screenSettings - это имя таблицы.

Теперь фиктивный класс можно передать в функцию Azure из модульного теста.

Может это то, что вы ищете?

Спасибо, это решение мне подходит. Мне просто пришлось добавить свойство Etag в «Результат», потому что моя функция обновляет таблицу.

Carlos Coelho 15.04.2019 20:01

Это отличный ответ. У меня были проблемы с форматом Uri. Как только я понял это правильно, я смог создать заглушку с NSubstitute без создания подклассов. Если вы пытаетесь имитировать TableQuerySegment<T>, здесь есть работающий метод github.com/Azure/azure-storage-net/issues/…

seangwright 24.04.2020 07:53

@wilco, он действительно использует фреймворк для фиксации или только этот MockCloudTable? Сможете ли вы сказать это с помощью MOQ

CredibleAshok 15.07.2021 06:47

Чтобы добавить к ответу здесь, поскольку вы стремитесь использовать фреймворк фиксации, простая настройка объекта, который наследуется от CloudTable и предоставляет конструктор по умолчанию, должна позволить вам макетировать сам унаследованный объект и контролировать то, что он возвращает:

public class CloudTableMock : CloudTable
{
    public CloudTableMock() : base(new Uri("http://127.0.0.1:10002/devstoreaccount1/screenSettings"))
    {
    }
}

Тогда это всего лишь случай создания макета. Я использую NSubstitute, поэтому сделал:

_mockTable = Substitute.For<CloudTableMock>();

но я предполагаю, что Moq позволит:

_mockTableRef = new Mock<CloudTable>();
_mockTableRef.Setup(x => x.DoSomething()).ReturnsAsync("My desired result");
_mockTable = _mockTableRef.Object;

(Мой Moq немного заржавел, поэтому я предполагаю, что приведенный выше синтаксис не совсем правильный)

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

CredibleAshok 15.07.2021 06:51

Вот моя реализация:

 public class StorageServiceTest
{
   IStorageService _storageService;
    Mock<CloudStorageAccount> _storageAccount;
    [SetUp]
    public void Setup()
    {
        var c = new StorageCredentials("dummyStorageAccountName","DummyKey");
       _storageAccount = new Mock<CloudStorageAccount>(c, true);
        _storageService = new StorageService(_storageAccount.Object);
    }

    [Test]
    [TestCase("ax0-1s", "random-1")]
    public void get_content_unauthorized(string containerName,string blobName)
    {
        //Arrange
        string expectOutputText = "Something on the expected blob";
        Uri uri = new Uri("https://somethig.com//");
        var blobClientMock = new Mock<CloudBlobClient>(uri);
        _storageAccount.Setup(a => a.CreateCloudBlobClient()).Returns(blobClientMock.Object);

        var cloudBlobContainerMock = new Mock<CloudBlobContainer>(uri);
        blobClientMock.Setup(a => a.GetContainerReference(containerName)).Returns(cloudBlobContainerMock.Object);

        var cloudBlockBlobMock = new Mock<CloudBlockBlob>(uri);
        cloudBlobContainerMock.Setup(a => a.GetBlockBlobReference(blobName)).Returns(cloudBlockBlobMock.Object);

        cloudBlockBlobMock.Setup(a => a.DownloadTextAsync()).Returns(Task.FromResult(expectOutputText));

        //Act
       var actual = _storageService.DownloadBlobAsString(containerName, blobName);

        //Assert
        Assert.IsNotNull(actual);
        Assert.IsFalse(string.IsNullOrWhiteSpace(actual.Result));
        Assert.AreEqual(actual.Result, expectOutputText);
    }
}

Реализация класса обслуживания:

 Task<string> IStorageService.DownloadBlobAsString(string containerName, string blobName)
    {
        var blobClient = this.StorageAccountClient.CreateCloudBlobClient();

        var blobContainer = blobClient.GetContainerReference(containerName);

        var blobReference = blobContainer.GetBlockBlobReference(blobName);

        var blobContentAsString = blobReference.DownloadTextAsync();

        return blobContentAsString;
    }

Я столкнулся с тем же сценарием, что и выбранный ответ, с использованием функции Azure с привязкой к таблице. Есть некоторые ограничения на использование фиктивного CloudTable, особенно при использовании System.Linq с CreateQuery<T>, например, поскольку это методы расширения на IQueryable.

Лучше использовать макет HttpMessageHandler, такой как RichardSzalay.MockHttp с TableClientConfiguration.RestExecutorConfiguration.DelegatingHandler, а затем просто заглушить ответ json, который вы ожидаете от таблицы.

public class Azure_Function_Test_With_Table_Binding
{
    [Fact]
    public void Should_be_able_to_stub_out_a_CloudTable()
    {
        var storageAccount = StorageAccount.NewFromConnectionString("UseDevelopmentStorage=true");
        var client = storageAccount.CreateCloudTableClient();

        var mockedRequest = new MockHttpMessageHandler()
            .When("http://127.0.0.1:10002/devstoreaccount1/pizzas*")
            .Respond("application/json", 
            @"{
                ""value"": [
                    {
                        ""Name"": ""Pepperoni"",
                        ""Price"": 9.99
                    }
                ]
            }");

        client.TableClientConfiguration.RestExecutorConfiguration.DelegatingHandler = new MockedRequestAdapter(mockedRequest);
        var table = client.GetTableReference("pizzas");

        var request = new DefaultHttpContext().Request;
        request.Query = new QueryCollection(new Dictionary<string, StringValues> { { "Pizza", new StringValues("Pepperoni") } });

        var result = PizzaStore.Run(request, table, null);

        Assert.IsType<OkObjectResult>(result);
    }
}

public class MockedRequestAdapter : DelegatingHandler
{
    private readonly MockedRequest _mockedRequest;

    public MockedRequestAdapter(MockedRequest mockedRequest) : base()
    {
        _mockedRequest = mockedRequest;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _mockedRequest.SendAsync(new HttpRequestMessage(request.Method, request.RequestUri), cancellationToken);
    }
}

public static class PizzaStore
{
    [FunctionName("PizzaStore")]
    public static IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
        [Table("pizzas", Connection = "AzureWebJobsStorage")] CloudTable cloud,
        ILogger log)
    {
        if (req.Query.TryGetValue("Pizza", out var value))
        {
            var pizza = cloud.CreateQuery<Pizza>().Where(p => p.Name == value.ToString()).SingleOrDefault();

            return new OkObjectResult(new { Pizza = pizza.Name, Price = pizza.Price });
        }

        return new NotFoundResult();
    }
}

public class Pizza : TableEntity
{
    public string Name { get; set; }
    public double Price { get; set; }
}

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