Поэтому я пытаюсь протестировать хранилище таблиц 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");
Любые идеи приветствуются!
@Ambidex извините за путаницу, есть идеи по поводу издевательства над ним?
Я отредактировал свой предыдущий комментарий. В основном в этом сценарии я бы предложил использовать фабрику для создания таблицы, а затем передать ее через конструктор класса TableStorage. Все классы в этом примере позволяют имитировать и использовать виртуальные методы. refactoring.guru/design-patterns/factory-method
@Ambidex, который кажется прямым подходом, я ищу способы издеваться над ним, как есть, без изменения поведения класса. Мне интересно, возможно ли это. Спасибо за разъяснения, ценю!
Ваш класс тесно связан с проблемами реализации, которые затрудняют изолированное модульное тестирование. Текущий дизайн тестируемого класса, вероятно, будет работать в интеграционном тесте, но для этого потребуется фактическое подключение к реальной облачной службе. Конструктор класса также вводит в заблуждение относительно его зависимостей, только явно введенный регистратор. Обратите внимание, что в большинстве случаев уровень сложности, возникающей при попытке протестировать класс, напрямую отражает чистоту дизайна.
Все эти проблемы, связанные с облаком, следует рассматривать как сторонние внешние зависимости и абстрагироваться.
Попробуйте что-нибудь вроде это. Это обертка вокруг реальной вещи, обеспечивающая интерфейсы, над которыми вы можете имитировать.
у меня аналогичное поведение, добавляю комментарий, чтобы следить за ним
@Nkosi, у тебя есть лучший дизайн этого. Я понимаю, когда вы говорите, что он тесно связан, но не могли бы вы привести пример этого со слабой связью?
@Coke, ты получил свой ответ, если да, то поделись им. заранее спасибо





Я также боролся с реализацией модульного теста для функции 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 в «Результат», потому что моя функция обновляет таблицу.
Это отличный ответ. У меня были проблемы с форматом Uri. Как только я понял это правильно, я смог создать заглушку с NSubstitute без создания подклассов. Если вы пытаетесь имитировать TableQuerySegment<T>, здесь есть работающий метод github.com/Azure/azure-storage-net/issues/…
@wilco, он действительно использует фреймворк для фиксации или только этот MockCloudTable? Сможете ли вы сказать это с помощью MOQ
Чтобы добавить к ответу здесь, поскольку вы стремитесь использовать фреймворк фиксации, простая настройка объекта, который наследуется от 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 немного заржавел, поэтому я предполагаю, что приведенный выше синтаксис не совсем правильный)
не могли бы вы добавить рабочий пример. Поскольку это просто ссылка, и я не могу это сделать.
Вот моя реализация:
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; }
}
Я смотрю на API, и класс не запечатан, и все методы в этом примере фактически виртуальные. Поэтому я не могу привести пример того, как это исправить, потому что мне кажется, что в этом нет никаких проблем. Я также действительно не понимаю, как вы можете реально смоделировать зависимости в этом примере. Вам действительно следует использовать фабричный объект, чтобы предоставить таблицу и передать ее через конструктор. Таким образом, остается только одна строка кода, и ее гораздо проще заменить в UnitTesting.