Azure Functions – Part 2 – Unit and Integration Testing

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Community Hub.

Unit Testing


Introduction

 

A unit test usually involves writing code to test a specific function or method, using input data and expected output data. The test code then executes the function or method with the input data, compares the resulting output to the expected output, and reports any discrepancies as errors or failures. This approach allows developers to test each unit of code in isolation, ensuring that their code is modular, well-designed, and functioning as intended.


For example, if you have a method that converts a full name to a first name and a last name and the input is John Doe, what you would want to validate is that the output of that method is John and Doe. If that breaks, then your assertion breaks, and you would need to fix your code. That’s the main idea behind any type of testing.

Unit testing is testing what is called the “unit”. A “unit” can be anything, but, usually, unit tests assess the method.


In unit testing any input and output (I/O) calls are mocked. I/O calls include:

  • External network API
  • Database
  • File system

 

The main idea behind mocking in unit testing is to avoid making calls to external components, such as APIs or databases, during testing. These calls can slow down unit tests, which should ideally be fast. Instead of setting up a whole environment to make these calls, developers can use mocks to simulate their behavior. Mocks replace the actual calls with simulated responses, allowing developers to test their code without relying on external components that may not always be available or consistent.

 

Why is unit testing important


The short execution time feedback loop, mocking dependencies and then getting a quick response on how your system performs is extremely powerful. It can catch bugs before they make it near production.

Isn't it enough to test the system manually instead of relying on automated unit testing?

If you do this every single time, you can’t possibly test every single scenario, every single time you make a code change, manually. You need an automated way to do that. A unit test which is fast and small. If a bug gets deployed into production and is found by a customer, then it would cost your company a lot of money, even more compared to the money spent for developers to write those unit tests in the first place.


Another advantage of unit tests is that they can be read as documentation. You can give very descriptive names in your unit tests and then you can just read the method names of the unit tests and understand what your application is doing.


They also “force” the developers to write better and cleaner code because unit testing needs some very specific techniques to be good, like SOLID principles, Inversion of Control, and Dependency Injection.


In most companies, unit tests are mandatory. They are run as part of the build pipeline and if at least one unit test fails, the whole build fails. You need to have a certain number of tests before you can push anything into production, thus proving that your application code is covered.


Finally, some people think unit testing is so important, that they choose to do what’s called “Test Driven Development” (TDD). TDD is a practice where you write your tests before you write your actual code.

Core unit testing concepts


In unit testing, you have three core concepts:

  • Testing Library:
    • The unit testing framework to use for writing your unit tests.
    • Examples for .NET:
  • Mocking Library:
    • The mocking framework to use for isolating your class / method under test from its dependencies and ensure that the proper methods on the dependent objects are being called.
    • Examples for .NET:
  • Assertion Library:
    • A set of (usually) extension methods that allow you to specify the expected outcome of a TDD or BDD-style unit test more naturally.
    • Examples for .NET:

Practical Example

To apply the concepts presented in this article, a sample API will be used. This API was developed using the HTTP-Trigger based Azure Function Template (C#) in VS2022. It contains a single POST endpoint, which you can call to create a Note. As part of this create note operation, the note is being persisted in Cosmos DB and then a simple noteCreated event is sent to a third party notification system.

The structure of the solution follows this format:

 

ormeikopmsft_0-1678972065397.png

 

The src solution folder contains the source code of the Azure Functions API project (“Fta.DemoFunc.Api”) and the tests solution folder contains the two test projects, one for Unit (Fta.DemoFunc.Api.Tests.Unit) and the other for Integration (Fta.DemoFunc.Api.Tests.Integration) tests respectively.

To be easier to write unit tests, the dependency injection (DI) software design pattern is being applied in the Azure Functions project, which is a technique to achieve Inversion of Control (IoC) between classes and their dependencies. Below you can see the Startup.cs file, containing the setup and configuration of the DI container.

ormeikopmsft_1-1678972065423.png

 

The Azure Functions project contains a single Function called NotesFunction. The “business logic” of the Azure Function has been extracted into a service called NoteService, which implements the interface called INoteService.

ormeikopmsft_2-1678972065453.png

 

As you can see in the image above the NotesFunction contains a single POST endpoint which accepts a CreateNoteRequest object from the client and delegates the note creation process to the service that implements the INoteService interface. As DI is applied to this class, now it is easy to write unit tests against it. The process you follow is the same as you would write unit tests for any other class.

As mentioned above, there are three core unit testing concepts: The Testing, the Mocking, and the Assertion Library. In this demo project, xUnit is used for the Testing Library, NSubstitute for the Mocking Library and Fluent Assertions for the Assertion Library.

To begin writing unit tests for the NotesFunction class, a new class called NotesFunctionTests is created inside the Fta.DemoFunc.Api.Tests.Unit project.

ormeikopmsft_3-1678972065462.png

 

Based on the above implementation, we need to write three unit tests to cover all scenarios for the POST method of our “System Under Test - SUT” (NotesFunction).

The first unit test will cover the scenario, where the NoteService is called and completed successfully, so our function will return a CreatedResult (201 status code) to the client along with the details of the created note.

ormeikopmsft_4-1678972065473.png

 

The second unit test will cover the scenario, where the NoteService returns null, so our function will return a BadRequestObjectResult (400 status code) to the client along with an error message.

ormeikopmsft_5-1678972065481.png

 

Finally, the third unit test will cover the scenario of an exception being thrown from the NoteService. In that case, we are going to log the error and just return an InternalServerErrorResult (500 status code) back to the client.

ormeikopmsft_6-1678972065490.png

 

As you can see above, all unit tests have very descriptive names and read like documentation. Also, each unit test follows the “AAA” pattern. “AAA” stands for “Arrange”, “Act” and “Assert”. In the “Arrange” part, you write initialization code for your unit test, in the “Act” part you call the method of your SUT you write your unit test against and in the “Assert” part you make your assertions, based on what is expected as the output of the unit test.

All code, examples and details for this project can be found in this GitHub repo.

Integration Testing

Introduction

Integration testing is the phase in software testing in which individual software modules are combined and evaluated as a group. It is conducted to evaluate the compliance of a system or component with specific functional requirements. It occurs after unit testing and before end-to-end testing.

Integration tests usually check for what we call the happy and unhappy path. There is also value in calling the database or API dependencies to make sure that they are behaving correctly. This is where integration tests come into the picture because they will call dependencies. This means that either you need to have a working and running environment for this, or you need to spin up one on demand (e.g., in docker). They are also bigger in scope compared to unit tests.

Why is integration testing important

If you are going through the full testing flow and you have your unit tests first, integration tests give you a better idea of how your system will perform when integrating with other components. You assume quite a lot in unit testing and that is not great when you want a realistic representation of your system.

Scope of integration tests

Let’s now examine what exactly is the scope of integration testing when it comes to things you are calling or mocking (i.e., file system, network calls, database calls). For example, let’s say you have an API that makes a few calls. One of them is to the GitHub API, another is to an internal API that you own, another is in the database and the final one is in the file system.

In the context of an integration test, the call to the database must happen, so you would use a realistic database and you would not replace that with anything “mocked”. The call to the file system must also happen.

The call to another API that your API needs to work with has some additional considerations to think about. If this is an API that you own (i.e., another API in your system), then you get to choose whether to run and call it or just mock it. On the other hand, if this is an external API that you do not own (e.g., the GitHub API), because you do not have any control over that, this is out of scope for your integration tests. Instead, you would replace that with an API that accepts requests and responds as if it was the GitHub API, ensuring that there is still some integration point with the same contracts / HTTP headers / models, etc. You can also choose to mock it, like you do in the unit testing way.

motasem13_1-1679401740307.png

 

Practical Example

Let’s examine how to apply the above concepts in the context of the example Azure Functions HTTP-Trigger based “Notes” API. To begin writing integration tests for the NotesFunction class, a new class called NotesFunctionTests is created inside the Fta.DemoFunc.Api.Tests.Integration project.

ormeikopmsft_8-1678972065514.png

 

In this example, we have two dependencies in our code: The Cosmos DB component where notes are persisted and the 3rd party API notification system. For the Cosmos DB case, as this is something that you own and control, you could spin up this dependency locally, if possible. You could choose to run your tests against a local Cosmos DB instance instead of a remote one. This has couple advantages:

  • Tests are faster to run locally than against a remote database.
  • You run tests independently from other developers because the test data from other machines will not impact your database.

In our example, we will run our integration tests against a local instance of Azure CosmosDB, using the Azure Cosmos DB Emulator.

The crucial part of our integration test setup is to configure dependency injection. You need the following classes for the setup:

  • TestStartup:
    • A new TestStartup.cs class will be introduced and will derive from the Azure Function’s Startup class to define dependency injection for our test.

ormeikopmsft_9-1678972065519.png

 

  • Configuration:
    • You should never store keys and secrets inside a git repository.
    • For local development in an Azure Functions project, you can use a local.settings.json to store configuration, which will never leave your local machine.

ormeikopmsft_10-1678972065522.png

 

  • TestsInitializer:

ormeikopmsft_11-1678972065526.png

 

ormeikopmsft_12-1678972065530.png

 

After setting up all the above components, we can continue with creating our integration tests. Based on the implementation of our NotesFunction class, we need to write two integration tests to cover the “happy” and the “unhappy” path for the POST method of our “System Under Test - SUT” (NotesFunction).

The first integration test will cover the “happy path” scenario, where the POST endpoint is called with valid note details, so our function will return a CreatedResult (201 status code) to the client along with the details of the created note.

ormeikopmsft_13-1678972065537.png

 

The second unit test will cover the scenario, where the POST endpoint is called with invalid note details, so our function will return a BadRequestObjectResult (400 status code) to the client along with some error message.

ormeikopmsft_14-1678972065541.png

 

As you can see above, all integration tests have very descriptive names and read like documentation. Also, each integration test follows the “AAA” pattern, just like unit tests.

All code, examples and details for this project can be found in this GitHub repo.

The Testing Pyramid

The “testing pyramid” is a visualization technique to see how important the distinct types of testing are and how much of it you need in a project. The pyramid has 3 levels:

  • At the foundation level, you have unit testing.
  • In the middle level you have integration testing.
  • At the top level you have end-to-end testing.

Unit tests are the larger number of tests you are going to have in your code, to cover any scenario that you need to validate against. Integration tests are a bit higher than unit tests, and you have less compared to unit tests, because you are testing a broader scenario in your application. Finally, end to end tests are the highest and the least because you are only evaluating the few things your application exposes.

motasem13_0-1679401698743.png

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.