I have an Azure Function written with .NET 8 and that uses the isolated worker model.
I want to write integration tests for this function and therefore want to spin it up in memory.
I looked into WebApplicationFactory
but it seems like it’s not supporting isolated worker model.
I came across Testcontainers
which sounds promising (but that also seems to not be 100% supported). But I’m giving it a try. For that to work I must dockerize my function and this is where I face issues. I had Visual Studio generate a Dockerfile. It builds, but when starting a container with the image it fails.
Before diving into the details of the error, please let me know if there is an alternative approach to solve this problem. I’m open to suggestions.
Dockerfile
It’s worth noting that I build the image from a different folder than my Dockerfile using the command: docker build -f "My Subfolder/Dockerfile" -t my-image:latest . --no-cache
.
The commented-out lines are additional things I tried:
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
#COPY --from=mcr.microsoft.com/dotnet/core/sdk:3.1 /usr/share/dotnet /usr/share/dotnet
WORKDIR /home/site/wwwroot
EXPOSE 8080
# Install Azure Functions Core Tools
RUN apt-get update &&
apt-get install -y curl &&
curl -sL https://deb.nodesource.com/setup_20.x | bash - &&
apt-get install -y nodejs &&
npm install -g azure-functions-core-tools@4 --unsafe-perm
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY . .
RUN dotnet restore "./My Subfolder/Project.csproj"
#COPY . .
WORKDIR "/src/My Subfolder"
#RUN dotnet build "./Project.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Project.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /home/site/wwwroot
COPY --from=publish /app/publish .
#RUN apt-get update &&
# apt-get install -y nodejs npm &&
# npm install -g azurite
#CMD ["ls"]
ENTRYPOINT ["func", "start", "--dotnet-isolated"]
ENV AzureWebJobsScriptRoot=/home/site/wwwroot
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
WEBSITES_INCLUDE_CLOUD_CERTS=true
Issues
-
One error seems to be that the function needs a storage account which it doesn’t have access to.
One thought is to install Azurite in the container, but I didn’t really get this to work and as I have multiple Azure functions that is set up in a similar way, they would all have their own installation.Another approach is using docker compose, but that’s not supported by the .NET version of Testcontainers yet.
So just to move on I removed the part that needs the storage (a TimerTrigger), so error is temporarily gone. -
Another error which is quite persistent is that it can’t seem to read my
settings.json
file. I use the options pattern where I have some validation, like:public sealed class MyOptions { [Required(ErrorMessage = "Option is required")] public string SomeOption { get; set; } = null!; }
I tried
CMD ["ls"]
in my final stage of the Dockerfile and it does list mysettings.json
file. It works fine locally, so not sure what’s going on here.The error looks like this:
2024-09-23 12:17:07 Azure Functions Core Tools 2024-09-23 12:17:07 Core Tools Version: 4.0.6280 Commit hash: N/A +421f0144b42047aa289ce691dc6db4fc8b6143e6 (64-bit) 2024-09-23 12:17:07 Function Runtime Version: 4.834.3.22875 2024-09-23 12:17:07 2024-09-23 12:17:08 [2024-09-23T10:17:08.173Z] Csproj not found in /home/site/wwwroot directory tree. Skipping user secrets file configuration. 2024-09-23 12:17:08 Skipping 'AzureWebJobsScriptRoot' from local settings as it's already defined in current environment variables. 2024-09-23 12:17:08 Skipping 'AZURE_FUNCTIONS_ENVIRONMENT' from local settings as it's already defined in current environment variables. 2024-09-23 12:17:09 2024-09-23 12:17:09 Functions: 2024-09-23 12:17:09 2024-09-23 12:17:09 MyHttpFunction: [GET,POST] http://localhost:7071/api/MyHttpFunction 2024-09-23 12:17:09 2024-09-23 12:17:09 For detailed output, run func with --verbose flag. 2024-09-23 12:17:09 [2024-09-23T10:17:09.841Z] MyOptions: 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'MyOptions' members: 'SomeOption' with the error: 'Option is required'. 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] at Microsoft.Extensions.Options.OptionsFactory`1.Create(String name) 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode) 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] at System.Lazy`1.CreateValue() 2024-09-23 12:17:10 [2024-09-23T10:17:10.154Z] at Microsoft.Extensions.Options.OptionsCache`1.GetOrAdd[TArg](String name, Func`3 createOptions, TArg factoryArgument) ...
Questions
Different Approach: Is there a different approach to solve this problem? I’m open to suggestions.
Storage Account: How can I handle the storage account requirement in a Docker container? Is installing Azurite the right approach?
Settings.json: How can it be that my settings.json file is not read correctly in the Docker container, and how can I fix this?
I’m not sure how to proceed. I’m not sure if I’m on the right track with Testcontainers and Docker. I’m open to suggestions on how to solve these problems. I’m also open to other approaches to write integration tests for my Azure Function using the isolated worder model.
2
Answers
For isolated worker model functions, the
AzureFunctions.TestHost
library offers a local testing option without Docker.Using Azurite in Docker is still one of the best solutions for simulating Azure Storage. However, to make it work smoothly, use Docker Compose to run both the function app and Azurite together,
Docker Compose:
Log:
I’ve spent a few days just whittling away at a solution for running my own self-hosted Azure Functions but I’m using Microsoft’s PowerShell image(s) without modification. If you need to run your own container, just sub in the name as required. All of the same options probably apply, so I’ll post my docker compose file for you and others to crib from.
Obviously you’ll need a folder structure for your bind mounts.
Note that when using a compose file, you don’t need to expose Azurite’s ports and can instead just define the endpoints in the connection string instead since the other services (Function App containers) can just lookup
azurite
and Docker’s DNS will point them to the correct container.To be clear, the AccountName and AccountKey as defined here are well known and are what you should actually use – ie. I’m not giving away any secrets, you actually need to use those values in the connection string.
Hopefully the bind mounts make sense without explanation but I should mention that I’m effectively sharing the secrets files (where the master key is) between both Function App instances. You may want to split that depending on your requirements.