DEV Community

Cover image for Integration Test for Azure Functions and Cosmos DB walkthrough
Alex Kondrashov
Alex Kondrashov

Posted on

Integration Test for Azure Functions and Cosmos DB walkthrough

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:

  1. Tests are faster to run locally rather than against a remote database.
  2. 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 };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Azure Cosmos DB Emulator in browser

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>();
      }
  }
Enter fullscreen mode Exit fullscreen mode

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=="
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

appsettings.json (stays in source control and is enreached by a CI/CD Pipeline)

{
  "Cars": {
    "Database": {
      "ConnectionString": ""
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the test

We can run our integration test with dotnet test command.

Result of running 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

  1. Install and develop locally with Azure Cosmos DB Emulator | Microsoft Learn.
  2. File transformation for application config.

Top comments (0)