Integration Test for Azure Functions and Cosmos DB walkthrough
Tags: dotnet, azurefunctions, testing, database
Published: 2022-12-27 18:33:53

## 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:

```csharp
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:

```csharp
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`:

```csharp
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:

![Azure Cosmos DB Emulator in browser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kb71nt5jstndsltrvy9n.png)

## 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.

```csharp
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](https://medium.com/r/?url=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fazure%2Fdevops%2Fpipelines%2Fprocess%2Fvariables%3Fview%3Dazure-devops%26tabs%3Dyaml%252Cbatch) and [FileTransform](https://medium.com/r/?url=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fazure%2Fdevops%2Fpipelines%2Ftasks%2Freference%2Ffile-transform-v1%3Fview%3Dazure-pipelines) in Azure Pipelines. [Here](https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D_iPfzH3ENAk) is the example of how can we achieve it.

`local.settings.json` (shouldn't leave your local machine):

```json
{
  "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)

```json
{
  "Cars": {
    "Database": {
      "ConnectionString": ""
    }
  }
}
```

### TestsInitializer
We want to use a test host for our _integration test_ using `TestStartup`.

```csharp
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.

```csharp
[CollectionDefinition(Name)]
public class IntegrationTestsCollection : ICollectionFixture<TestsInitializer>
{
    public const string Name = nameof(IntegrationTestsCollection);
}
```

### Integration Test
We can finally implement our `integration test`:

```csharp
[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.

![Result of running dotnet test command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ioj8y7ud180kzu9i3hf.png)

## 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](https://medium.com/r/?url=https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fazure%2Fcosmos-db%2Flocal-emulator%3Ftabs%3Dssl-netstd21).
2. [File transformation for application config](https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D_iPfzH3ENAk).