Integration Test for Azure Functions and Cosmos DB walkthrough
Alex Kondrashov
Posted on December 27, 2022
To Long To Read
There is a big advantage of running integration tests with a straightforward setup. It hels you finding bugs earlier in the process and it gives you insurance that code works as expected. Follow my guide below to run your integration test for Azure Function and Cosmos DB.
More
Integration test - is a type of software testing in which the different units, modules or components of a software application are tested as a combined entity.
It's a good pracrice to spin up dependencies locally if possible. You should choose running tests agains local database instead of a remote one. This has couple advantages:
- Tests are faster to run locally rather than against a remote database.
- You run tests independantly from other developers. The test data from other machines will not impact your database.
System under test
Azure Functions is a cloud service available on-demand that provides all the continually updated infrastructure and resources needed to run your applications. You focus on the code that matters most to you, in the most productive language for you, and Functions handles the rest.
To this example I've put togther an Azure Function. It exposes an endpoint to Create cars:
public class CarFunction
{
private readonly ICarRepository _carRepository;
private readonly ILogger<CarFunction> _logger;
public CarFunction(ICarRepository carRepository, ILogger<CarFunction> logger)
{
_carRepository = carRepository ?? throw new ArgumentNullException(nameof(carRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
[FunctionName("CreateCar")]
public async Task<IActionResult> CreateCar([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "cars")] CarRequest request, HttpRequest req)
{
try
{
if (string.IsNullOrWhiteSpace(request.Name))
return new BadRequestObjectResult("Name is mandatory.");
var createdCar = await _carService.CreateCar(request);
return new CreatedResult("/cars/" + createdCar.Id, createdCar);
}
catch (Exception ex)
{
return new ObjectResult(ex.Message) { StatusCode = 500 };
}
}
}
We will be using Cosmos DB to store cars. Here is how the repository looks:
public class CarRepository : ICarRepository
{
private CosmosClient _cosmosClient;
private Container _container;
public CarRepository(IOptions<Configuration> configuration)
{
this._cosmosClient = new CosmosClient(configuration.Value.ConnectionString);
this._container = _cosmosClient.GetContainer("CarDatabase", "Cars");
}
public async Task<Car> Create(Car car)
{
var itemResponse = await _container.CreateItemAsync(car, new PartitionKey(car.Id));
return itemResponse.Resource;
}
}
Since we use function app we have to have Startup.cs
:
public class Startup : FunctionsStartup
{
private const string configurationSection = "Cars:Database";
protected virtual IConfigurationRoot GetConfigurationRoot(IFunctionsHostBuilder functionsHostBuilder)
{
var local = new ConfigurationBuilder()
.AddJsonFile(Path.Combine(Environment.CurrentDirectory, "local.settings.json"), true, true)
.AddEnvironmentVariables()
.Build();
return local;
}
public override void Configure(IFunctionsHostBuilder builder)
{
var local = GetConfigurationRoot(builder);
var config = new ConfigurationBuilder().AddEnvironmentVariables();
var configurationSection = local.GetSection(configurationSection);
builder.Services.Configure<Configuration>(configurationSection);
var configuration = config.Build();
builder.Services.AddInfrastructure(configuration);
}
}
Azure Cosmos DB Emulator
We will run our intergation test against local instance of Azure CosmosDB. It's availalble for download here. Here is how it will look once you launch it:
Dependency Injection
The crutual part of integration test setup is to confige dependency injection. You need the following classes for the setup:
TestStartup
We will derived from Startup
class to define dependency injection for our test.
public class TestStartup : Startup
{
protected override IConfigurationRoot GetConfigurationRoot(IFunctionsHostBuilder functionsHostBuilder)
{
var currentDirectory = AppDomain.CurrentDomain.BaseDirectory;
var configuration = new ConfigurationBuilder()
.SetBasePath(currentDirectory)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile("local.settings.json", true, true)
.AddEnvironmentVariables()
.Build();
return configuration;
}
public override void Configure(IFunctionsHostBuilder builder)
{
base.Configure(builder);
builder.Services.AddTransient<CarsFunction>();
}
}
Configuration
It's not advisable to store keys and secrets in git repository. For local development we can use local.settings.json
to store configuration. Yet we can use appsettings.json
to manage configuration. For example, we could use pipeline variables and FileTransform in Azure Pipelines. Here is the example of how can we achieve it.
local.settings.json
(shouldn't leave your local machine):
{
"Cars": {
"Database": {
"ConnectionString": "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
}
}
}
appsettings.json
(stays in source control and is enreached by a CI/CD Pipeline)
{
"Cars": {
"Database": {
"ConnectionString": ""
}
}
}
TestsInitializer
We want to use a test host for our integration test using TestStartup
.
public class TestsInitializer
{
public TestsInitializer()
{
var host = new HostBuilder()
.ConfigureWebJobs(builder => builder.UseWebJobsStartup(typeof(TestStartup), new WebJobsBuilderContext(), NullLoggerFactory.Instance))
.Build();
ServiceProvider = host.Services;
}
public IServiceProvider ServiceProvider { get; }
}
We also need to include a Collection definition by deriving from ICollectionFixture
class.
[CollectionDefinition(Name)]
public class IntegrationTestsCollection : ICollectionFixture<TestsInitializer>
{
public const string Name = nameof(IntegrationTestsCollection);
}
Integration Test
We can finally implement our integration test
:
[Collection(IntegrationTestsCollection.Name)]
public class CarFunctionTests : IClassFixture<TestStartup>, IAsyncLifetime
{
private CarFunction _carFunction;
private readonly TestsInitializer _testsInitializer;
private readonly CosmosClient _cosmosClient;
private Container _container;
private readonly string _carId;
public CarFunctionTests(TestsInitializer testsInitializer)
{
_testsInitializer = testsInitializer;
var cosmosDatabaseConfiguration = testsInitializer.ServiceProvider.GetService<IOptions<CarConfiguration>>();
_cosmosClient = new CosmosClient(cosmosDatabaseConfiguration.Value.EndpointUri, cosmosDatabaseConfiguration.Value.PrimaryKey);
_carFunction = _testsInitializer.ServiceProvider.GetService<CarFunction>();
}
[Fact]
public async void TestCreateCar()
{
// Arrange
var carName = $"BMW - {Guid.NewGuid()}";
var carRequest = new CarRequest { Name = carName };
// Act
var response = await _carFunction.CreateCar(, new DefaultHttpContext().Request);
var createdResponse = (CreatedResult)_response;
_carId = (createdResponse.Value as Car).Id;
// Assert
Assert.IsType<CreatedResult>(createdResponse);
Assert.Equal(carName, (createdResponse.Value as Car).Name);
}
public async Task InitializeAsync()
{
var databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync("CarDatabase");
var database = databaseResponse.Database;
var containerResponse = await database.CreateContainerIfNotExistsAsync("Cars", "/id");
_container = containerResponse.Container;
}
public async Task DisposeAsync()
{
await _container.DeleteItemAsync<Car>(_carId, new PartitionKey(_carId));
}
}
Running the test
We can run our integration test with dotnet test
command.
Summary
We've written an integration test for Azure Function and Cosmos DB. We also have used replaceable configuration and configured dependency injection to work for us.
Resources
Posted on December 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.