问题
This is the first time I am writing test case and I am not sort of stuck and not sure how to proceed further.
I have the following API. In the below sample I have 2 endpoints which I want to perform testing.
public class ValuesController : Controller
{
//This interface is used to setup dynamo db and connection to aws
private IDynamoDbClientInitialization _clientAccessor;
private static string dynamoDbTable = string.Empty;
public ValuesController(IOptions<Dictionary<string, string>> appSettings, IDynamoDbClientInitialization clientAccessor)
{
var vals = appSettings.Value;
dynamoDbTable = vals["dynamoDbTable"];
_clientAccessor = clientAccessor;
}
[HttpGet("api/data")]
public async Task<List<MyModel>> GetAllData(string type, string status)
{
List<ScanCondition> conditions = new List<ScanCondition>();
conditions.Add(new ScanCondition("Type", ScanOperator.Equal, type));
conditions.Add(new ScanCondition("Status", ScanOperator.Equal, status));
var response = await _clientAccessor.GetContext().ScanAsync<MyModel>(conditions, AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync();
return results.Select(x => x.UpdatedBy.ToLower()).ToList();
}
[HttpPost("api/save")]
public async Task<IActionResult> SaveData([FromBody] List<MyModel> listData, string input, string name, string type)
{
List<MyModel> model = null;
foreach (var data in listData)
{
//populating data here
await _clientAccessor.GetContext().SaveAsync(data, AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable));
}
return Ok();
}
}
public class DynamoDbClientInitialization : IDynamoDbClientInitialization
{
private readonly DynamoDbClientSettings settings;
private DynamoDBContext _awsContext;
public DynamoDbClientInitialization(IOptions<DynamoDbClientSettings> options)
{
settings = options?.Value;
}
public DynamoDBContext GetContext()
{
//Check is context already exists. If not create a new one.
if(_awsContext != null)
{
return _awsContext;
}
else
{
var creds = AWSHelperMethods.SetAwsCredentials(settings.Id, settings.Password);
var dynamoClient = AWSHelperMethods.GetDynamoDbClient(creds, settings.Region);
_awsContext = AWSHelperMethods.GetDynamoDbContext(dynamoClient);
return _awsContext;
}
}
}
public static class AWSHelperMethods
{
public static BasicAWSCredentials SetAwsCredentials(string awsId, string awsPassword)
{
var creds = new BasicAWSCredentials(awsId, awsPassword);
return creds;
}
public static AmazonDynamoDBClient GetDynamoDbClient(BasicAWSCredentials creds, RegionEndpoint awsDynamoDbRegion)
{
var client = new AmazonDynamoDBClient(creds, awsDynamoDbRegion);
return client;
}
public static DynamoDBContext GetDynamoDbContext(AmazonDynamoDBClient client)
{
var context = new DynamoDBContext(client);
return context;
}
public static DynamoDBOperationConfig GetDynamoDbOperationConfig(string dynamoDbTable)
{
DynamoDBOperationConfig config = new DynamoDBOperationConfig() { OverrideTableName = dynamoDbTable };
return config;
}
}
Below is the xunit project that I added. Here I am using MOQ to moq up my aws connection and others. Questions are below in comments against the code.
public class DataTest
{
[Fact]
public void PassingTest()
{
//Arrange
var dynamoDbTable = "someValue";
//Trying to moq IOptions
var moqOp = new Mock<IOptions<Dictionary<string, string>>>();
//Create an instance to hold desired values
var vals = new Dictionary<string, string>();
//Set expected value
vals["dynamoDbTable"] = dynamoDbTable;
//Setup dependency behavior
moqOp.Setup(_ => _.Value).Returns(vals);
//Trying to moq my connection
var moqDb = new Mock<IDynamoDbClientInitialization>();
//Fake data
List<MyModel> data = new List<MyModel>()
{
//populate as needed
};
moqDb
.Setup(_ => _.GetContext().ScanAsync<MyModel>
(It.IsAny<List<ScanCondition>>(), AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync())
.ReturnsAsync(data);
ValuesController controller = new ValuesController(moqOp.Object,
moqDb.Object);
var actual = controller.GetAllData();
}
}
Above I am getting the error as: An expression tree may not contain a call or invocation that uses optional arguments
This is on line
.Setup(_ => _.GetContext().ScanAsync<MyModel>
(It.IsAny<List<ScanCondition>>(), AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync())
Can anyone help to resolve?
---updated---
public interface IDynamoDbManager
{
Task<List<T>> GetAsync(IEnumerable<ScanCondition> conditions);
Task SaveAsync(T item);
}
回答1:
An expression tree may not contain a call or invocation that uses optional arguments
You are trying to "call" a method ScanAsync on not already set up GetContext(). To solve this you have to Setup return value for GetContext() before you tried to Setup ScanAsync()
This code is very hard to test so let's refactor it.
You have to change direct calls of _clientAccessor.GetContext()
to injection of IDynamoDBContext.
DynamoDbClientInitialization doesn't make sense as can be easily replaced with IAmazonDynamoDb. To get rid of verbose config reading code lines, use
services.AddAWSService<IAmazonDynamoDB>();
All calls to DynamoDb should be encapsulated on a separated class called, for example, DynamoDbManager
public class DynamoDbManager<T> : DynamoDBContext, IDynamoDbManager<T> where T : class
{
private DynamoDBOperationConfig _config;
public DynamoDbManager(IAmazonDynamoDB client, string tableName): base(client)
{
_config = new DynamoDBOperationConfig()
{
OverrideTableName = tableName
};
}
public Task<List<T>> GetAsync(IEnumerable<ScanCondition> conditions)
{
return ScanAsync<T>(conditions, _config).GetRemainingAsync();
}
public Task SaveAsync(T item)
{
return base.SaveAsync(item, _config);
}
}
Now your controller will be changed in this way ValuesController
public class ValuesController : Controller
{
private readonly IDynamoDbManager<MyModel> _dynamoDbManager;
//This interface is used to setup dynamo db and connection to aws
private static string dynamoDbTable = string.Empty;
public ValuesController(IOptions<Dictionary<string, string>> appSettings, IDynamoDbManager<MyModel> dynamoDbManager)
{
_dynamoDbManager = dynamoDbManager;
var vals = appSettings.Value;
dynamoDbTable = vals["dynamoDbTable"];
}
[HttpGet("api/data")]
public async Task<IActionResult> GetAllData(string type, string status)
{
var conditions = new List<ScanCondition>
{
new ScanCondition("Type", ScanOperator.Equal, type),
new ScanCondition("Status", ScanOperator.Equal, status)
};
var result = await _dynamoDbManager.GetAsync(conditions);
var response = result.Select(_ => _.UpdatedBy.ToLower()).ToList();
return Ok(response);
}
[HttpPost("api/save")]
public async Task<IActionResult> SaveData([FromBody] List<MyModel> listData, string input, string name, string type)
{
foreach (var data in listData)
{
//populating data here
await _dynamoDbManager.SaveAsync(data);
}
return Ok();
}
}
Refactoring is finished, start writing unit tests ValuesControllerTests
public class ValuesControllerTests
{
private Mock<IDynamoDbManager<MyModel>> _dbManager;
private ValuesController _valuesController;
public ValuesControllerTests()
{
var mockRepository = new MockRepository(MockBehavior.Loose);
_dbManager = mockRepository.Create<IDynamoDbManager<MyModel>>();
var options = new OptionsWrapper<Dictionary<string, string>>(new Dictionary<string, string>()
{
{"dynamoDbTable", nameof(MyModel) }
});
_valuesController = new ValuesController(options, _dbManager.Object);
}
[Fact]
public async Task GetAllData_ShouldSelectUpdateByInLowerCase()
{
//
var searchResult = new List<MyModel>()
{
new MyModel() {UpdatedBy = "UpdatedBy1"}
};
_dbManager
.Setup(_ => _.GetAsync(It.Is<List<ScanCondition>>(list => list.Count == 2)))
.ReturnsAsync(searchResult);
//
var result = await _valuesController.GetAllData("typeValue", "statusValue");
//
var okResult = result as OkObjectResult;
Assert.NotNull(okResult);
var values = okResult.Value as List<string>;
Assert.Contains("updatedby1", values);
}
[Fact]
public async Task SaveData_ShouldCallSaveForAllRequestedData()
{
//
var listData = new List<MyModel>()
{
new MyModel(),
new MyModel(),
new MyModel()
};
_dbManager
.Setup(_ => _.SaveAsync(It.IsAny<MyModel>()))
.Returns(Task.CompletedTask);
//
var result = await _valuesController.SaveData(listData, "","", "");
//
_dbManager.Verify(_ => _.SaveAsync(It.IsAny<MyModel>()), Times.Exactly(3));
}
}
That's all, I did two tests for each controller action because its far more understandable - if you realy need only one test you can easily join them
来源:https://stackoverflow.com/questions/52879141/net-core-testing-with-xunit