Skip to content

Commit 3cf340d

Browse files
committed
Merge pull request #187708 from diberry/diberry/0207-azure-function-context
Azure Functions - JS Context
2 parents 2db34ca + 29da316 commit 3cf340d

File tree

4 files changed

+271
-416
lines changed

4 files changed

+271
-416
lines changed

.openpublishing.redirection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5938,6 +5938,11 @@
59385938
"redirect_url": "/azure/azure-app-configuration/policy-reference",
59395939
"redirect_document_id": false
59405940
},
5941+
{
5942+
"source_path_from_root": "/articles/azure-functions/functions-test-a-function.md",
5943+
"redirect_url": "/azure/azure-functions/supported-languages",
5944+
"redirect_document_id": false
5945+
},
59415946
{
59425947
"source_path_from_root": "/articles/azure-app-configuration/quickstart-azure-function-csharp.md",
59435948
"redirect_url": "/azure/azure-app-configuration/quickstart-azure-functions-csharp",

articles/azure-functions/functions-dotnet-class-library.md

Lines changed: 238 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Understand how to use C# to develop and publish code as class libra
55
ms.topic: conceptual
66
ms.devlang: csharp
77
ms.custom: devx-track-csharp
8-
ms.date: 07/24/2021
8+
ms.date: 02/08/2022
99

1010
---
1111
# Develop C# class library functions using Azure Functions
@@ -336,17 +336,6 @@ namespace ServiceBusCancellationToken
336336
}
337337
```
338338

339-
As in the previous example, you commonly iterate through an array using a `foreach` loop. Within this loop and before processing the message, you should check the value of `cancellationToken.IsCancellationRequested` to see if cancellation is pending. In the case where `IsCancellationRequested` is `true`, you might need to take some actions to prepare for a graceful shutdown. For example, you might want to log the status of your code before the shutdown, or perhaps write to a persisted store the portion of the message batch which hasn't yet been processed. If you write this kind of information to a persisted store, your startup code needs to check the store for any unprocessed message batches that were written during shutdown. What your code needs to do during graceful shutdown depends on your specific scenario.
340-
341-
Azure Event Hubs is an other trigger that supports batch processing messages. The following example is a function method definition for an Event Hubs trigger with a cancellation token that accepts an incoming batch as an array of [EventData](/dotnet/api/microsoft.azure.eventhubs.eventdata) objects:
342-
343-
```csharp
344-
public async Task Run([EventHubTrigger("csharpguitar", Connection = "EH_CONN")]
345-
EventData[] events, CancellationToken cancellationToken, ILogger log)
346-
```
347-
348-
The pattern to process a batch of Event Hubs events is similar to the previous example of processing a batch of Service Bus messages. In each case, you should check the cancellation token for a cancellation state before processing each item in the array. When a pending shutdown is detected in the middle of the batch, handle it gracefully based on your business requirements.
349-
350339
## Logging
351340

352341
In your function code, you can write output to logs that appear as traces in Application Insights. The recommended way to write to the logs is to include a parameter of type [ILogger](/dotnet/api/microsoft.extensions.logging.ilogger), which is typically named `log`. Version 1.x of the Functions runtime used `TraceWriter`, which also writes to Application Insights, but doesn't support structured logging. Don't use `Console.Write` to write your logs, since this data isn't captured by Application Insights.
@@ -580,6 +569,243 @@ Don't call `TrackRequest` or `StartOperation<RequestTelemetry>` because you'll s
580569
Don't set `telemetryClient.Context.Operation.Id`. This global setting causes incorrect correlation when many functions are running simultaneously. Instead, create a new telemetry instance (`DependencyTelemetry`, `EventTelemetry`) and modify its `Context` property. Then pass in the telemetry instance to the corresponding `Track` method on `TelemetryClient` (`TrackDependency()`, `TrackEvent()`, `TrackMetric()`). This method ensures that the telemetry has the correct correlation details for the current function invocation.
581570

582571

572+
## Testing functions in C# in Visual Studio
573+
574+
The following example describes how to create a C# Function app in Visual Studio and run and tests with [xUnit](https://github.com/xunit/xunit).
575+
576+
![Testing Azure Functions with C# in Visual Studio](./media/functions-test-a-function/azure-functions-test-visual-studio-xunit.png)
577+
578+
### Setup
579+
580+
To set up your environment, create a Function and test app. The following steps help you create the apps and functions required to support the tests:
581+
582+
1. [Create a new Functions app](functions-get-started.md) and name it **Functions**
583+
2. [Create an HTTP function from the template](functions-get-started.md) and name it **MyHttpTrigger**.
584+
3. [Create a timer function from the template](functions-create-scheduled-function.md) and name it **MyTimerTrigger**.
585+
4. [Create an xUnit Test app](https://xunit.net/docs/getting-started/netcore/cmdline) in the solution and name it **Functions.Tests**.
586+
5. Use NuGet to add a reference from the test app to [Microsoft.AspNetCore.Mvc](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc/)
587+
6. [Reference the *Functions* app](/visualstudio/ide/managing-references-in-a-project) from *Functions.Tests* app.
588+
589+
### Create test classes
590+
591+
Now that the projects are created, you can create the classes used to run the automated tests.
592+
593+
Each function takes an instance of [ILogger](/dotnet/api/microsoft.extensions.logging.ilogger) to handle message logging. Some tests either don't log messages or have no concern for how logging is implemented. Other tests need to evaluate messages logged to determine whether a test is passing.
594+
595+
You'll create a new class named `ListLogger` which holds an internal list of messages to evaluate during a testing. To implement the required `ILogger` interface, the class needs a scope. The following class mocks a scope for the test cases to pass to the `ListLogger` class.
596+
597+
Create a new class in *Functions.Tests* project named **NullScope.cs** and enter the following code:
598+
599+
```csharp
600+
using System;
601+
602+
namespace Functions.Tests
603+
{
604+
public class NullScope : IDisposable
605+
{
606+
public static NullScope Instance { get; } = new NullScope();
607+
608+
private NullScope() { }
609+
610+
public void Dispose() { }
611+
}
612+
}
613+
```
614+
615+
Next, create a new class in *Functions.Tests* project named **ListLogger.cs** and enter the following code:
616+
617+
```csharp
618+
using Microsoft.Extensions.Logging;
619+
using System;
620+
using System.Collections.Generic;
621+
using System.Text;
622+
623+
namespace Functions.Tests
624+
{
625+
public class ListLogger : ILogger
626+
{
627+
public IList<string> Logs;
628+
629+
public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;
630+
631+
public bool IsEnabled(LogLevel logLevel) => false;
632+
633+
public ListLogger()
634+
{
635+
this.Logs = new List<string>();
636+
}
637+
638+
public void Log<TState>(LogLevel logLevel,
639+
EventId eventId,
640+
TState state,
641+
Exception exception,
642+
Func<TState, Exception, string> formatter)
643+
{
644+
string message = formatter(state, exception);
645+
this.Logs.Add(message);
646+
}
647+
}
648+
}
649+
```
650+
651+
The `ListLogger` class implements the following members as contracted by the `ILogger` interface:
652+
653+
- **BeginScope**: Scopes add context to your logging. In this case, the test just points to the static instance on the `NullScope` class to allow the test to function.
654+
655+
- **IsEnabled**: A default value of `false` is provided.
656+
657+
- **Log**: This method uses the provided `formatter` function to format the message and then adds the resulting text to the `Logs` collection.
658+
659+
The `Logs` collection is an instance of `List<string>` and is initialized in the constructor.
660+
661+
Next, create a new file in *Functions.Tests* project named **LoggerTypes.cs** and enter the following code:
662+
663+
```csharp
664+
namespace Functions.Tests
665+
{
666+
public enum LoggerTypes
667+
{
668+
Null,
669+
List
670+
}
671+
}
672+
```
673+
674+
This enumeration specifies the type of logger used by the tests.
675+
676+
Now create a new class in *Functions.Tests* project named **TestFactory.cs** and enter the following code:
677+
678+
```csharp
679+
using Microsoft.AspNetCore.Http;
680+
using Microsoft.AspNetCore.Http.Internal;
681+
using Microsoft.Extensions.Logging;
682+
using Microsoft.Extensions.Logging.Abstractions;
683+
using Microsoft.Extensions.Primitives;
684+
using System.Collections.Generic;
685+
686+
namespace Functions.Tests
687+
{
688+
public class TestFactory
689+
{
690+
public static IEnumerable<object[]> Data()
691+
{
692+
return new List<object[]>
693+
{
694+
new object[] { "name", "Bill" },
695+
new object[] { "name", "Paul" },
696+
new object[] { "name", "Steve" }
697+
698+
};
699+
}
700+
701+
private static Dictionary<string, StringValues> CreateDictionary(string key, string value)
702+
{
703+
var qs = new Dictionary<string, StringValues>
704+
{
705+
{ key, value }
706+
};
707+
return qs;
708+
}
709+
710+
public static HttpRequest CreateHttpRequest(string queryStringKey, string queryStringValue)
711+
{
712+
var context = new DefaultHttpContext();
713+
var request = context.Request;
714+
request.Query = new QueryCollection(CreateDictionary(queryStringKey, queryStringValue));
715+
return request;
716+
}
717+
718+
public static ILogger CreateLogger(LoggerTypes type = LoggerTypes.Null)
719+
{
720+
ILogger logger;
721+
722+
if (type == LoggerTypes.List)
723+
{
724+
logger = new ListLogger();
725+
}
726+
else
727+
{
728+
logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
729+
}
730+
731+
return logger;
732+
}
733+
}
734+
}
735+
```
736+
737+
The `TestFactory` class implements the following members:
738+
739+
- **Data**: This property returns an [IEnumerable](/dotnet/api/system.collections.ienumerable) collection of sample data. The key value pairs represent values that are passed into a query string.
740+
741+
- **CreateDictionary**: This method accepts a key/value pair as arguments and returns a new `Dictionary` used to create `QueryCollection` to represent query string values.
742+
743+
- **CreateHttpRequest**: This method creates an HTTP request initialized with the given query string parameters.
744+
745+
- **CreateLogger**: Based on the logger type, this method returns a logger class used for testing. The `ListLogger` keeps track of logged messages available for evaluation in tests.
746+
747+
Finally, create a new class in *Functions.Tests* project named **FunctionsTests.cs** and enter the following code:
748+
749+
```csharp
750+
using Microsoft.AspNetCore.Mvc;
751+
using Microsoft.Extensions.Logging;
752+
using Xunit;
753+
754+
namespace Functions.Tests
755+
{
756+
public class FunctionsTests
757+
{
758+
private readonly ILogger logger = TestFactory.CreateLogger();
759+
760+
[Fact]
761+
public async void Http_trigger_should_return_known_string()
762+
{
763+
var request = TestFactory.CreateHttpRequest("name", "Bill");
764+
var response = (OkObjectResult)await MyHttpTrigger.Run(request, logger);
765+
Assert.Equal("Hello, Bill. This HTTP triggered function executed successfully.", response.Value);
766+
}
767+
768+
[Theory]
769+
[MemberData(nameof(TestFactory.Data), MemberType = typeof(TestFactory))]
770+
public async void Http_trigger_should_return_known_string_from_member_data(string queryStringKey, string queryStringValue)
771+
{
772+
var request = TestFactory.CreateHttpRequest(queryStringKey, queryStringValue);
773+
var response = (OkObjectResult)await MyHttpTrigger.Run(request, logger);
774+
Assert.Equal($"Hello, {queryStringValue}. This HTTP triggered function executed successfully.", response.Value);
775+
}
776+
777+
[Fact]
778+
public void Timer_should_log_message()
779+
{
780+
var logger = (ListLogger)TestFactory.CreateLogger(LoggerTypes.List);
781+
MyTimerTrigger.Run(null, logger);
782+
var msg = logger.Logs[0];
783+
Assert.Contains("C# Timer trigger function executed at", msg);
784+
}
785+
}
786+
}
787+
```
788+
789+
The members implemented in this class are:
790+
791+
- **Http_trigger_should_return_known_string**: This test creates a request with the query string values of `name=Bill` to an HTTP function and checks that the expected response is returned.
792+
793+
- **Http_trigger_should_return_string_from_member_data**: This test uses xUnit attributes to provide sample data to the HTTP function.
794+
795+
- **Timer_should_log_message**: This test creates an instance of `ListLogger` and passes it to a timer functions. Once the function is run, then the log is checked to ensure the expected message is present.
796+
797+
If you want to access application settings in your tests, you can [inject](functions-dotnet-dependency-injection.md) an `IConfiguration` instance with mocked environment variable values into your function.
798+
799+
### Run tests
800+
801+
To run the tests, navigate to the **Test Explorer** and click **Run all**.
802+
803+
![Testing Azure Functions with C# in Visual Studio](./media/functions-test-a-function/azure-functions-test-visual-studio-xunit.png)
804+
805+
### Debug tests
806+
807+
To debug the tests, set a breakpoint on a test, navigate to the **Test Explorer** and click **Run > Debug Last Run**.
808+
583809
## Environment variables
584810

585811
To get an environment variable or an app setting value, use `System.Environment.GetEnvironmentVariable`, as shown in the following code example:

0 commit comments

Comments
 (0)