Skip to content

Instantly share code, notes, and snippets.

@justinyoo
Last active August 13, 2021 17:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save justinyoo/c06c3b4df77d4fc6075f924e19ec0d6a to your computer and use it in GitHub Desktop.
Save justinyoo/c06c3b4df77d4fc6075f924e19ec0d6a to your computer and use it in GitHub Desktop.
Azure Functions SRE, The First Cut
# Release Pipeline
- stage: Release
jobs:
- deployment: HostedVs2017
...
variables:
- name: TestRunStatus
value: ''
strategy:
runOnce:
deploy:
steps:
...
- task: CmdLine@2
displayName: 'Run E2E Tests'
inputs:
script: 'dotnet vstest $(Pipeline.Workspace)\e2e\FunctionApp.Tests\FunctionApp.Tests.dll --testCaseFilter:TestCategory=E2E --logger:trx --resultsDirectory:$(Pipeline.Workspace)\TestResults'
continueOnError: true
env:
ServerName: FunctionApp
FunctionAuthKey: $(FunctionAuthKey)
FunctionAppName: $(FunctionAppName)
- task: PowerShell@2
displayName: 'Save Test Run Status'
inputs:
targetType: Inline
script: 'Write-Host "##vso[task.setvariable variable=TestRunStatus]$(Agent.JobStatus)"'
- task: PublishTestResults@2
displayName: 'Publish E2E Test Results'
inputs:
testRunTitle: 'E2E Tests'
testResultsFormat: VSTest
testResultsFiles: '$(Pipeline.Workspace)/TestResults/*.trx'
mergeTestResults: true
- task: PowerShell@2
displayName: 'Cancel Pipeline on Test Run Failure'
condition: and(succeeded(), or(eq(variables['TestRunStatus'], 'Failed'), eq(variables['TestRunStatus'], 'SucceededWithIssues')))
inputs:
targetType: Inline
script: |
Write-Host "##vso[task.setvariable variable=Agent.JobStatus]Failed"
Write-Host "##vso[task.complete result=Failed]DONE"
dotnet test [Test_Project_Name].csproj -c Release --filter:"TestCategory=E2E"
set ServerName=FunctionApp
set FunctionAuthKey=uCQISIGoaMYz6d/6yR/Q2aw6PVdxLhzOh1gy9IjfoRbTN9OgmOFgTQ==
set FunctionAppName=fncapp-mountebank
public class FunctionAppServerFixture : ServerFixture
{
private const string FunctionAppNameKey = "FunctionAppName";
private const string FunctionAuthKeyKey = "FunctionAuthKey";
private readonly string _functionAppName;
private readonly string _functionAuthKey;
public FunctionAppServerFixture()
{
this._functionAppName = Environment.GetEnvironmentVariable(FunctionAppNameKey);
this._functionAuthKey = Environment.GetEnvironmentVariable(FunctionAuthKeyKey);
}
public override string GetHealthCheckUrl(HttpStatusCode statusCode = HttpStatusCode.OK)
{
return $"https://{this._functionAppName}.azurewebsites.net/api/ping?code={this._functionAuthKey}";
}
}
public interface IHealthCheckFunction : IFunction<ILogger>
{
}
public class HealthCheckFunction : FunctionBase<ILogger>, IHealthCheckFunction
{
...
// Dependency injections here
public override async Task<TOutput> InvokeAsync<TInput, TOutput>(
TInput input,
functionOptionsBase options = null)
{
var result = (IActionResult)null;
var requestUri = $"{this._settings.BaseUri.TrimEnd('/')}/{this._settings.Endpoints.HealthCheck.TrimStart('/')}";
using (var response = await this._httpClient.GetAsync(requestUri).ConfigureAwaitfalse))
{
try
{
response.EnsureSuccessStatusCode();
result = new OkResult();
}
catch (Exception ex)
{
var error = new ErrorResponse(ex);
result = new ObjectResult(error) { StatusCode = (int)response.StatusCode };
}
}
return (TOutput)result;
}
}
[TestClass]
public class HealthCheckHttpTriggerTests
{
private const string CategoryIntegration = "Integration";
private const string CategoryE2E = "E2E";
private const string DefaultServerName = "Localhost";
private const string ServerNameKey = "ServerName";
private ServerFixture _fixture;
[TestInitialize]
public void Init()
{
// Gets the server name from the environment variable.
var serverName = Environment.GetEnvironmentVariable(ServerNameKey);
if (string.IsNullOrWhiteSpace(serverName))
{
serverName = DefaultServerName;
}
// Creates the fixture from the factory method pattern.
this._fixture = ServerFixture.CreateInstance(serverName);
}
...
[TestMethod]
[TestCategory(CategoryIntegration)]
// Adds the test category for end-to-end testing
[TestCategory(CategoryE2E)]
public async Task Given_Url_When_Invoked_Then_Trigger_Should_Return_Healthy()
{
...
}
}
public class HealthCheckHttpTrigger
{
...
// Dependency injections here
[FunctionName(nameof(HealthCheckHttpTrigger.PingAsync))]
public async Task<IActionResult> PingAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "ping")] HttpRequest req,
ILogger log)
{
return result = await this._function
.InvokeAsync<HttpRequest, IActionResult>(req)
.ConfigureAwait(false);
}
}
npm install -g mountebank
stages:
# Build Pipeline
- stage: Build
jobs:
- job: HostedVs2017
...
variables:
- name: IntegrationTestRunStatus
value: ''
steps:
...
- task: CmdLine@2
displayName: 'Integration Test Function App'
inputs:
script: 'dotnet test $(Build.SourcesDirectory)\test\FunctionApp.Tests\FunctionApp.Tests.csproj -c $(BuildConfiguration) --filter:"TestCategory=Integration" --logger:trx --results-directory:$(System.DefaultWorkingDirectory)\IntegrationTestResults'
env:
ServerName: Localhost
continueOnError: true
- task: PowerShell@2
displayName: 'Save Integration Test Run Status'
inputs:
targetType: Inline
script: 'Write-Host "##vso[task.setvariable variable=IntegrationTestRunStatus]$(Agent.JobStatus)"'
- task: PublishTestResults@2
displayName: 'Publish Integration Test Results'
inputs:
testRunTitle: 'Integration Tests'
testResultsFormat: VSTest
testResultsFiles: '$(System.DefaultWorkingDirectory)/IntegrationTestResults/*.trx'
mergeTestResults: true
- task: PowerShell@2
displayName: 'Cancel Pipeline on Test Run Failure'
condition: and(succeeded(), or(eq(variables['UnitTestRunStatus'], 'Failed'), eq(variables['UnitTestRunStatus'], 'SucceededWithIssues'), eq(variables['IntegrationTestRunStatus'], 'Failed'), eq(variables['IntegrationTestRunStatus'], 'SucceededWithIssues')))
inputs:
targetType: Inline
script: |
Write-Host "##vso[task.setvariable variable=Agent.JobStatus]Failed"
Write-Host "##vso[task.complete result=Failed]DONE"
...
dotnet test [Test_Project_Name].csproj -c Release --filter:"TestCategory=Integration"
[TestClass]
public class HealthCheckHttpTriggerTests
{
private const string CategoryIntegration = "Integration";
private ServerFixture _fixture;
[TestInitialize]
public void Init()
{
this._fixture = new LocalhostServerFixture();
}
[TestMethod]
[TestCategory(CategoryIntegration)]
public async Task Given_Url_When_Invoked_Then_Trigger_Should_Return_Healthy()
{
// Arrange
var uri = this._fixture.GetHealthCheckUrl();
using (var http = new HttpClient())
// Act
using (var res = await http.GetAsync(uri))
{
// Assert
res.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
[TestMethod]
[TestCategory(CategoryIntegration)]
public async Task Given_Url_When_Invoked_Then_Trigger_Should_Return_Unhealthy()
{
// Arrange
var uri = this._fixture.GetHealthCheckUrl(HttpStatusCode.InternalServerError);
using (var http = new HttpClient())
// Act
using (var res = await http.GetAsync(uri))
{
// Assert
res.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
}
}
}
public class LocalhostServerFixture : ServerFixture
{
...
public override string GetHealthCheckUrl(HttpStatusCode statusCode = HttpStatusCode.OK)
{
...
}
}
public class LocalhostServerFixture
{
private readonly MountebankClient _client;
public MountebankServerFixture()
{
this._client = new MountebankClient();
}
public string GetHealthCheckUrl(HttpStatusCode statusCode = HttpStatusCode.OK)
{
this._client.DeleteImposter(8080);
var imposter = this._client
.CreateHttpImposter(8080, statusCode.ToString());
imposter.AddStub()
.OnPathAndMethodEqual("/api/ping", Method.Get)
.ReturnsStatus(statusCode);
this._client.Submit(imposter);
return "http://localhost:7071/api/ping";
}
}
start /b mb --noLogFile
start /b func host start --csharp
# Console #1
mb --noLogFile
# Console #2
func host start --csharp
public abstract class ServerFixture
{
public static ServerFixture CreateInstance(string serverName)
{
var type = Type.GetType($"FunctionApp.Tests.Fixtures.{serverName}ServerFixture");
var instance = (ServerFixture)Activator.CreateInstance(type);
return instance;
}
public abstract string GetHealthCheckUrl(HttpStatusCode statusCode = HttpStatusCode.OK);
}
stages:
# Build Pipeline
- stage: Build
jobs:
- job: HostedVs2017
...
variables:
- name: UnitTestRunStatus
value: ''
steps:
...
- task: CmdLine@2
displayName: 'Unit Test Function App'
inputs:
script: 'dotnet test $(Build.SourcesDirectory)\test\FunctionApp.Tests\FunctionApp.Tests.csproj -c $(BuildConfiguration) --filter:"TestCategory!=Integration&TestCategory!=E2E" --logger:trx --results-directory:$(System.DefaultWorkingDirectory)\UnitTestResults /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=$(System.DefaultWorkingDirectory)\CoverageResults\coverage'
env:
ServerName: Localhost
continueOnError: true
- task: PowerShell@2
displayName: 'Save Unit Test Run Status'
inputs:
targetType: Inline
script: 'Write-Host "##vso[task.setvariable variable=UnitTestRunStatus]$(Agent.JobStatus)"'
- task: PublishTestResults@2
displayName: 'Publish Unit Test Results'
inputs:
testRunTitle: 'Unit Tests'
testResultsFormat: VSTest
testResultsFiles: '$(System.DefaultWorkingDirectory)/UnitTestResults/*.trx'
mergeTestResults: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish Code Coverage Results'
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/CoverageResults/*.xml'
failIfCoverageEmpty: false
...
dotnet test [Test_Project_Name].csproj -c Release
[TestMethod]
public async Task Given_Parameters_When_Invoked_Then_InvokeAsync_Should_Return_Result()
{
// Arrange
var result = new OkResult();
var function = new Mock<IHealthCheckFunction>();
function.Setup(p => p.InvokeAsync<HttpRequest, IActionResult>(It.IsAny<HttpRequest>(), It.IsAny<FunctionOptionsBase>()))
.ReturnsAsync(result);
var trigger = new HealthCheckHttpTrigger(function.Object);
var req = new Mock<HttpRequest>();
var log = new Mock<ILogger>();
// Action
var response = await trigger.PingAsync(req.Object, log.Object).ConfigureAwait(false);
// Assert
response
.Should().BeOfType<OkResult>()
.And.Subject.As<OkResult>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
}
[TestMethod]
public async Task Given_Parameters_When_Invoked_Then_InvokeAsync_Should_Return_Result()
{
// Arrange
var result = new OkResult();
var function = new Mock<IHealthCheckFunction>();
function.Setup(p => p.InvokeAsync<HttpRequest, IActionResult>(It.IsAny<HttpRequest>(), It.IsAny<FunctionOptionsBase>()))
.ReturnsAsync(result);
var trigger = new HealthCheckHttpTrigger(function.Object);
var req = new Mock<HttpRequest>();
var log = new Mock<ILogger>();
// Action
var response = await trigger.PingAsync(req.Object, log.Object).ConfigureAwait(false);
// Assert
response
.Should().BeOfType<OkResult>()
.And.Subject.As<OkResult>()
.StatusCode.Should().Be((int)HttpStatusCode.OK);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment