C# 模拟Azure表存储的CloudStorageAccount和CloudTable

C# 模拟Azure表存储的CloudStorageAccount和CloudTable,c#,azure,unit-testing,mocking,azure-table-storage,C#,Azure,Unit Testing,Mocking,Azure Table Storage,所以我尝试测试Azure表存储并模拟我所依赖的东西。我的类的构造方式是在构造函数中建立连接,即创建CloudStorageAccount的新实例,其中创建了StorageCredentials的实例,该实例具有storageName和storageKey。之后,我创建了CloudTable的一个实例,我在代码中进一步使用它来执行CRUD操作。我的班级情况如下: public class TableStorage : ITableStorage { private const st

所以我尝试测试Azure表存储并模拟我所依赖的东西。我的类的构造方式是在构造函数中建立连接,即创建
CloudStorageAccount
的新实例,其中创建了
StorageCredentials
的实例,该实例具有
storageName
storageKey
。之后,我创建了
CloudTable
的一个实例,我在代码中进一步使用它来执行CRUD操作。我的班级情况如下:

public class TableStorage : ITableStorage
{
        private const string _records = "myTable";
        private CloudStorageAccount _storageAccount;
        private CloudTable _table;
        private ILogger<TableStorage> _logger;

        public AzureTableStorageService(ILogger<TableStorage> loggingService)
        {
            _storageAccount = new CloudStorageAccount(new StorageCredentials(
                 ConfigurationManager.azureTableStorageName, ConfigurationManager.azureTableStorageKey), true);
            _table = _storageAccount.CreateCloudTableClient().GetTableReference(_records);
            _table.CreateIfNotExistsAsync();
            _logger = loggingService;
        }

        //...
        //Other methods here
}
因此,当我尝试以这种方式构建单元测试时,我得到: 模拟的类型必须是接口、抽象类或非密封类。

我的目标是实现以下目标:

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

任何想法都将受到高度赞赏

我也在为绑定到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
        });
    }
}
在本例中,“screenSettings”是表的名称

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; }
}
模拟类现在可以从单元测试传递到Azure函数


也许这就是您想要的?

要在这里添加答案,因为您打算使用模拟框架,只需设置一个从CloudTable继承并提供默认构造函数的对象,就可以模拟继承的对象本身并控制它返回的内容:

public class CloudTableMock : CloudTable
{
    public CloudTableMock() : base(new Uri("http://127.0.0.1:10002/devstoreaccount1/screenSettings"))
    {
    }
}
然后就是创建模拟的一个例子。我正在使用NSubstitute,所以我做了:

_mockTable = Substitute.For<CloudTableMock>();
\u mockTable=Substitute.For();
但我猜最低起订量将允许:

_mockTableRef = new Mock<CloudTable>();
_mockTableRef.Setup(x => x.DoSomething()).ReturnsAsync("My desired result");
_mockTable = _mockTableRef.Object;
\u mockTableRef=new Mock();
_mockTableRef.Setup(x=>x.DoSomething()).ReturnsAsync(“我想要的结果”);
_mockTable=\u 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;
    }
公共类StorageServiceTest
{
IStorageService storageService;
模拟存储帐户;
[设置]
公共作废设置()
{
var c=新的存储凭据(“dummyStorageAccountName”、“DummyKey”);
_storageAccount=新模拟(c,true);
_storageService=新的storageService(\u storageAccount.Object);
}
[测试]
[测试用例(“ax0-1s”、“random-1”)]
public void get_content_unauthorized(字符串containerName、字符串blobName)
{
//安排
string expectOutputText=“预期blob上的某物”;
Uri=新的Uri(“https://somethig.com//");
var blobClientMock=新模拟(uri);
_Setup(a=>a.CreateCloudBlobClient()).Returns(blobClientLock.Object);
var cloudBlobContainerMock=新模拟(uri);
Setup(a=>a.GetContainerReference(containerName)).Returns(cloudBlobContainerMock.Object);
var cloudBlockBlobMock=新模拟(uri);
Setup(a=>a.GetBlockBlobReference(blobName)).Returns(cloudBlockBlobLock.Object);
Setup(a=>a.DownloadTextAsync()).Returns(Task.FromResult(expectOutputText));
//表演
var实际值=_storageService.DownloadBlobAsString(containerName,blobName);
//断言
Assert.IsNotNull(实际值);
Assert.IsFalse(string.IsNullOrWhiteSpace(actual.Result));
Assert.AreEqual(actual.Result,expectOutputText);
}
}
服务类实现:

 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;
    }
Task-IStorageService.DownloadBlobAsString(string-containerName,string-blobName)
{
var blobClient=this.StorageAccountClient.CreateCloudBlobClient();
var blobContainer=blobClient.GetContainerReference(containerName);
var blobReference=blobContainer.GetBlockBlobReference(blobName);
var blobContentAsString=blobreeference.DownloadTextAsync();
返回BlobContent字符串;
}

我遇到了与所选答案相同的场景,其中涉及带有表绑定的Azure函数。使用mock
CloudTable
有一些限制,特别是在使用
System.Linq
CreateQuery
时,例如,这些是
IQueryable
上的扩展方法

更好的方法是使用
HttpMessageHandler
mock,例如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; }
}
带有表绑定的公共类Azure函数测试
{
[事实]
public void应该能够删除CloudTable()
{
var storageAccount=storageAccount.NewFromConnectionString(“UseDevelopmentStorage=true”);
var client=storageAccount.CreateCloudTableClient();
var mockedRequest=new MockHttpMessageHandler()
.当http://127.0.0.1:10002/devstoreaccount1/pizzas*")
.Respond(“应用程序/json”,
@"{
“价值”:[
{
“名字”:“意大利香肠”,
“价格”:9.99
}
]
}");
client.TableClientConfiguration.RestExecutorConfiguration.DelegatingHandler=新的MockedRequestAdapter(mockedRequest);
var table=client.GetTableReference(“pizzas”);
var request=新的DefaultHttpContext().request;
request.Query=newquerycollection(新字典{{“Pizza”,新StringValues(“Pepperoni”)});
var result=PizzaStore.Run(请求、表、空);
Assert.IsType(结果);
}
}
公共类MockedRequestAdapter:DelegatingHandler
{
私有只读MockedRequest\u MockedRequest;
公共MockedRequestAdapter(MockedRequest MockedRequest):base()
{
_mockedRequest=mockedRequest;
}
受保护的覆盖异步任务SendAsync(HttpRequestMessage请求,CancellationToken CancellationToken)
{
返回等待\u模拟请求