|
| 1 | +# Azure Functions Core Tools E2E Testing Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This project contains the E2E tests for Azure Functions Core Tools. When adding a new E2E test, please follow these guidelines to ensure consistency and efficient test execution. |
| 6 | + |
| 7 | +### Test Organization |
| 8 | + |
| 9 | +- Create tests within `Commands/Func[COMMAND_NAME]` directories |
| 10 | +- Separate tests into categories by organizing them in files with similar tests |
| 11 | + - Examples: `AuthTests`, `LogLevelTests`, etc. |
| 12 | +- This organization allows tests in different files to run in parallel, improving execution speed |
| 13 | + |
| 14 | +### Test Types for `func start` |
| 15 | + |
| 16 | +There are two main types of tests for the `func start` command: |
| 17 | + |
| 18 | +#### 1. Tests with Fixtures |
| 19 | + |
| 20 | +- Based on `BaseFunctionAppFixture` |
| 21 | +- The fixture handles initialization (`func init` and `func new`) |
| 22 | +- Best for tests that require a simple `HttpTrigger` with no additional configuration |
| 23 | +- Add your test to the appropriate fixture to avoid setup overhead |
| 24 | + |
| 25 | +#### 2. Tests without Fixtures |
| 26 | + |
| 27 | +- Based on `BaseE2ETests` |
| 28 | +- Sets up variables like `WorkingDirectory` and `FuncPath` |
| 29 | +- You must handle application setup in each test |
| 30 | +- Use the provided helper methods `FuncInitWithRetryAsync` and `FuncNewWithRetryAsync` for setup |
| 31 | + |
| 32 | +### Important Notes |
| 33 | + |
| 34 | +### Test Parallelization |
| 35 | + |
| 36 | +❗ **Dotnet isolated templates and dotnet in-proc templates CANNOT be run in parallel** |
| 37 | + |
| 38 | +- Use traits to distinguish between dotnet-isolated and dotnet-inproc tests |
| 39 | +- This ensures they run sequentially rather than in parallel |
| 40 | + |
| 41 | +### Node.js Tests |
| 42 | + |
| 43 | +⚠️ **Node.js tests require explicit environment variable configuration** |
| 44 | + |
| 45 | +- Due to flakiness in `func init` and `func new` not properly initializing environment variables |
| 46 | +- Always append `.WithEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "node")` to the `funcStartCommand` like this: |
| 47 | + |
| 48 | +```csharp |
| 49 | +var result = funcStartCommand |
| 50 | + .WithWorkingDirectory(WorkingDirectory) |
| 51 | + .WithEnvironmentVariable("FUNCTIONS_WORKER_RUNTIME", "node") |
| 52 | + .Execute(commandArgs.ToArray()); |
| 53 | +``` |
| 54 | + |
| 55 | +## Step-by-Step Guide: Creating a `func start` Test without a Fixture |
| 56 | + |
| 57 | +### 1. Get an Available Port |
| 58 | + |
| 59 | +First, get an available port using `ProcessHelper.GetAvailablePort()` to ensure your test doesn't conflict with other processes: |
| 60 | + |
| 61 | +```csharp |
| 62 | +int port = ProcessHelper.GetAvailablePort(); |
| 63 | +``` |
| 64 | + |
| 65 | +### 2. Define Test Names |
| 66 | + |
| 67 | +Create descriptive names for your test. For parameterized tests, include the parameters in the test name for better traceability: |
| 68 | + |
| 69 | +```csharp |
| 70 | +string methodName = "Start_DotnetIsolated_Test_EnableAuthFeature"; |
| 71 | +string uniqueTestName = $"{methodName}_{parameterValue1}_{parameterValue2}"; |
| 72 | +``` |
| 73 | + |
| 74 | +### 3. Initialize a Function App |
| 75 | + |
| 76 | +Use `FuncInitWithRetryAsync` to create a new function app with the appropriate runtime: |
| 77 | + |
| 78 | +```csharp |
| 79 | +await FuncInitWithRetryAsync(uniqueTestName, new[] { ".", "--worker-runtime", "dotnet-isolated" }); |
| 80 | +``` |
| 81 | + |
| 82 | +### 4. Add a Trigger Function |
| 83 | + |
| 84 | +Use `FuncNewWithRetryAsync` to add a trigger function with specific configuration: |
| 85 | + |
| 86 | +```csharp |
| 87 | +await FuncNewWithRetryAsync(uniqueTestName, new[] { |
| 88 | + ".", |
| 89 | + "--template", "HttpTrigger", |
| 90 | + "--name", "HttpTrigger", |
| 91 | + "--authlevel", authLevel |
| 92 | +}); |
| 93 | +``` |
| 94 | + |
| 95 | +### 5. Create a FuncStartCommand |
| 96 | + |
| 97 | +Initialize a `FuncStartCommand` with the path to the func executable, test name, and logger: |
| 98 | + |
| 99 | +```csharp |
| 100 | +var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log); |
| 101 | +``` |
| 102 | + |
| 103 | +### 6. Add a Process Started Handler (Optional) |
| 104 | + |
| 105 | +If you need to wait for the host to start or check logs from a different process, add a `ProcessStartedHandler`: |
| 106 | + |
| 107 | +```csharp |
| 108 | +funcStartCommand.ProcessStartedHandler = async (process) => |
| 109 | +{ |
| 110 | + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger"); |
| 111 | +}; |
| 112 | +``` |
| 113 | + |
| 114 | +The `ProcessStartedHandlerHelper` method: |
| 115 | +- Waits for the host to start |
| 116 | +- Makes an HTTP request to the function |
| 117 | +- Captures logs from the process |
| 118 | +- Returns when the function has processed the request |
| 119 | + |
| 120 | +### 7. Build Command Arguments |
| 121 | + |
| 122 | +Build your command arguments based on test parameters: |
| 123 | + |
| 124 | +```csharp |
| 125 | +var commandArgs = new List<string> { "start", "--verbose", "--port", port.ToString() }; |
| 126 | +if (enableSomeFeature) |
| 127 | +{ |
| 128 | + commandArgs.Add("--featureFlag"); |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +### 8. Execute the Command |
| 133 | + |
| 134 | +Execute the command with the working directory and arguments: |
| 135 | + |
| 136 | +```csharp |
| 137 | +var result = funcStartCommand |
| 138 | + .WithWorkingDirectory(WorkingDirectory) |
| 139 | + .Execute(commandArgs.ToArray()); |
| 140 | +``` |
| 141 | + |
| 142 | +### 9. Validate the Results |
| 143 | + |
| 144 | +Validate the command output contains the expected results based on test parameters: |
| 145 | + |
| 146 | +```csharp |
| 147 | +if (someCondition) |
| 148 | +{ |
| 149 | + result.Should().HaveStdOutContaining("expected output 1"); |
| 150 | +} |
| 151 | +else |
| 152 | +{ |
| 153 | + result.Should().HaveStdOutContaining("expected output 2"); |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +## Testing with Fixtures |
| 158 | + |
| 159 | +The steps are similar for creating a `func start` test with a fixture, except you can skip the setup logic that calls `func init` and `func new` as the fixture handles this for you. |
| 160 | + |
| 161 | +## Complete Example |
| 162 | + |
| 163 | +```csharp |
| 164 | +public async Task Start_DotnetIsolated_Test_EnableAuthFeature( |
| 165 | + string authLevel, |
| 166 | + bool enableAuth, |
| 167 | + string expectedResult) |
| 168 | +{ |
| 169 | + int port = ProcessHelper.GetAvailablePort(); |
| 170 | + string methodName = "Start_DotnetIsolated_Test_EnableAuthFeature"; |
| 171 | + string uniqueTestName = $"{methodName}_{authLevel}_{enableAuth}"; |
| 172 | + |
| 173 | + // Setup the function app |
| 174 | + await FuncInitWithRetryAsync(uniqueTestName, new[] { ".", "--worker-runtime", "dotnet-isolated" }); |
| 175 | + await FuncNewWithRetryAsync(uniqueTestName, new[] { ".", "--template", "HttpTrigger", "--name", "HttpTrigger", "--authlevel", authLevel }); |
| 176 | + |
| 177 | + // Create and configure the start command |
| 178 | + var funcStartCommand = new FuncStartCommand(FuncPath, methodName, Log); |
| 179 | + funcStartCommand.ProcessStartedHandler = async (process) => |
| 180 | + { |
| 181 | + await ProcessHelper.ProcessStartedHandlerHelper(port, process, funcStartCommand.FileWriter, "HttpTrigger"); |
| 182 | + }; |
| 183 | + |
| 184 | + // Build command arguments |
| 185 | + var commandArgs = new List<string> { "start", "--verbose", "--port", port.ToString() }; |
| 186 | + if (enableAuth) |
| 187 | + { |
| 188 | + commandArgs.Add("--enableAuth"); |
| 189 | + } |
| 190 | + |
| 191 | + // Execute and validate |
| 192 | + var result = funcStartCommand |
| 193 | + .WithWorkingDirectory(WorkingDirectory) |
| 194 | + .Execute(commandArgs.ToArray()); |
| 195 | + |
| 196 | + if (string.IsNullOrEmpty(expectedResult)) |
| 197 | + { |
| 198 | + result.Should().HaveStdOutContaining("\"status\": \"401\""); |
| 199 | + } |
| 200 | + else |
| 201 | + { |
| 202 | + result.Should().HaveStdOutContaining("Selected out-of-process host."); |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
0 commit comments