.Net Core 2.1 TDD HttpClient and Http Requests Testing

.Net Core TDD – HttpClient and Http Requests Testing

Note: Using .net core 2.1

Using XUnit, but the concepts will remain the same no matter what testing framework you use.

Using Moq:

https://github.com/Moq/moq4/wiki/Quickstart

https://documentation.help/Moq/

There is also a really great free moq course here:

https://www.udemy.com/moq-framework

Tip:

Become familiar with Dependency Injection and Inversion Control in code so you can mock behaviour in tests.
Then become familiar with mocking in tests and assert behaviour based on mocked data or methods.

You can see working code here:

Github code:

https://github.com/CariZa/XUnit-Test-Samples

https://github.com/CariZa/XUnit-Test-Samples/blob/master/HTTPRequestsTests/RequestsTests.cs

HTTP Testing

Make use of dependency injection and inversion of control by creating a, HttpClient Handler.

HttpClient Handler

This handler will allow us to mock the behaviour of the HttpClient when we write out tests.

Interface (IHttpClientHandler.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace HTTPRequests
{
    public interface IHttpClientHandler
    {
        HttpResponseMessage Get(string url);
        HttpResponseMessage Post(string url, HttpContent content);
        Task<HttpResponseMessage> GetAsync(string url);
        Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
    }
}

Class (HttpClientHandler.cs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace HTTPRequests
{
    public class HttpClientHandler : IHttpClientHandler
    {
        private HttpClient _client = new HttpClient();

        public HttpResponseMessage Get(string url)
        {
            return GetAsync(url).Result;
        }

        public async Task<HttpResponseMessage> GetAsync(string url)
        {
            return await _client.GetAsync(url);
        }

        public HttpResponseMessage Post(string url, HttpContent content)
        {
            return PostAsync(url, content).Result;
        }

        public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
        {
            return await _client.PostAsync(url, content);
        }
    }
}

Requests

Create a Requests class for your http requests and inject the handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace HTTPRequests
{
    public class Requests
    {
        private readonly IHttpClientHandler _httpClient;

        public Requests(IHttpClientHandler httpClient) {
            _httpClient = httpClient;
        }

        public async Task<string> GetData(string baseUrl)
        {

            IHttpClientHandler client = _httpClient;
     
            using (HttpResponseMessage res = await client.GetAsync(baseUrl))
            try
            {
                using (HttpContent content = res.Content)
                {
                    string data = await content.ReadAsStringAsync();
                    if (data != null)
                    {
                        return data;
                    }
                    else
                    {
                        return "err no data";
                    }
                }
            }
            catch (Exception e)
            {
                return "err no content";
            }

        }

        public async Task<List<TodoModel>> GetTodosByUserId(string url, int userId)
        {
            var task = GetData(url);

            List<TodoModel> todos = null;
            await task.ContinueWith((jsonString) =>
              {
                  todos = JsonConvert.DeserializeObject<List<TodoModel>>(jsonString.Result);
                  todos = todos.FindAll(x => x.userId == userId);
              });
            return todos;
        }
    }
}

Use it in your code:

Using the above Requests class in action:

1
2
3
    Requests req = new Requests(new HttpClientHandler());
    string todoItem = req.GetData("https://jsonplaceholder.typicode.com/todos/1").Result;
    List<TodoModel> todos = req.GetTodosByUserId("https://jsonplaceholder.typicode.com/todos", 1).Result;

Writing tests

Testing the Http Requests using dependency injection and inversion of control:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using HTTPRequests;
using Moq;
using Xunit;
using Newtonsoft.Json;

namespace HTTPRequestsTests
{

    public class RequestsTests //: IClassFixture<RequestsTestsFixture>
    {
        [Fact]
        public void GetData_CheckGetAsyncIsCalled()
        {
            // Arrange/Setup
            //var moqRes = new Mock<HttpResponseMessage>();
            var moqHttp = new Mock<HTTPRequests.IHttpClientHandler>();
            moqHttp.Setup(HttpHandler => HttpHandler.GetAsync(It.IsAny<string>()))
                   .Returns(() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
            var req = new Requests(moqHttp.Object);
            var url = "testurl";

            // Act
            var todo = req.GetData(url);

            // Assert
            moqHttp.Verify(moqHttpInst => moqHttpInst.GetAsync(It.IsAny<string>()), Times.Exactly(1));
        }

        [Fact]
        public void GetData_CheckGetAsyncIsCalled_EmptyContent()
        {
            // Arrange/Setup
            var response = new HttpResponseMessage( HttpStatusCode.OK );
            var moqHttp = new Mock<HTTPRequests.IHttpClientHandler>();
            moqHttp.Setup(HttpHandler => HttpHandler.GetAsync(It.IsAny<string>()))
                   .Returns(() => Task.FromResult(response) );
            var req = new Requests(moqHttp.Object);
            var url = "testurl";

            // Act
            var todo = req.GetData(url);
            Console.WriteLine("Ending here after expection");

            // Assert
            Assert.True(todo.IsCompleted);

        }

        [Fact]
        public void GetData_CheckGetAsyncIsCalled_ReturnsString()
        {
            // Arrange/Setup

            // Content = new StringContent(SerializedString, System.Text.Encoding.UTF8, "application/json")
            var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Content here 123") };
            var moqHttp = new Mock<HTTPRequests.IHttpClientHandler>();
            moqHttp.Setup(HttpHandler => HttpHandler.GetAsync(It.IsAny<string>()))
                   .Returns(() => Task.FromResult(response));
            var req = new Requests(moqHttp.Object);
            var url = "testurl";

            // Act
            var todo = req.GetData(url);
            Console.WriteLine(todo.Result);

            //
            Assert.Equal("Content here 123", todo.Result);
        }

        [Fact]
        public void GetData_CheckGetAsyncIsCalled_ReturnsJSON()
        {
            // Arrange/Setup
            var mockJson = "{"GroupId":1,"Samples":[{"SampleId":1},{"SampleId":2}]}";
            var JSONContent = new StringContent(mockJson, System.Text.Encoding.UTF8, "application/json");
            var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = JSONContent };
            var moqHttp = new Mock<HTTPRequests.IHttpClientHandler>();
            moqHttp.Setup(HttpHandler => HttpHandler.GetAsync(It.IsAny<string>()))
                   .Returns(() => Task.FromResult(response));
            var req = new Requests(moqHttp.Object);
            var url = "testurl";

            // Act
            var todo = req.GetData(url);
            Console.WriteLine(todo.Result);

            // Assert
            Assert.Equal(mockJson, todo.Result);
        }
    }
}

.Net Core 2.1 TDD Database Requests

.Net Core TDD Databases

Note: This is using .net core 2.1

Using XUnit, but the concepts will remain the same no matter what testing framework you use.

Using Moq:

https://github.com/Moq/moq4/wiki/Quickstart

https://documentation.help/Moq/

There is also a really great free moq course here:

https://www.udemy.com/moq-framework

Tip:

Become familiar with Dependency Injection and Inversion Control in code so you can mock behaviour in tests.
Then become familiar with mocking in tests and assert behaviour based on mocked data or methods.

Github repo:

You can see working code here

https://github.com/CariZa/XUnit-CRUD-Example
https://github.com/CariZa/XUnit-CRUD-Example/tree/master/CRUD_Tests

Database Mocking in .net core

Models

Types of tests you could write to test a model:

Test a model can be created by creating an instance and testing the fields have been added to the model instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Fact]
public void BookModel_Instantiates()
{
            string book = "Harry Potter";
            string author = "JK Rowling";
            string isbn = "123234345";

            Book bookInst = new Book() {
                Name = book,
                Author = author,
                ISBN = isbn
            };

            Assert.Matches(bookInst.Name, book);
            Assert.Matches(bookInst.Author, author);
            Assert.Matches(bookInst.ISBN, isbn);

            // Check no validation errors
            Assert.False(ValidateModel(bookInst).Count > 0);
}

Validate using ValidateModel

Test validations for models using ValidateModel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        [Fact]
        public void BookModel_RequiresNameField()
        {
            string author = "JK Rowling";
            string isbn = "123234345";

            Book bookInst = new Book()
            {
                Author = author,
                ISBN = isbn
            };

            var invalidFields = ValidateModel(bookInst);

            // Validation errors should return
            Assert.True(invalidFields.Count > 0);
        }

        [Fact]
        public void BookModel_DoesNotRequireOtherFields()
        {
            string book = "Harry Potter";
            Book bookInst = new Book()
            {
                Name = book
            };

            var invalidFields = ValidateModel(bookInst);
            Assert.False(invalidFields.Count > 0);
        }

Validation Helper:

Also use this Helper method for the validation checks:

1
2
3
4
5
6
7
8
9
        // Validation Helper
        private IList<ValidationResult> ValidateModel(object model)
        {
            var validationResults = new List<ValidationResult>();
            var ctx = new ValidationContext(model, null, null);
            Validator.TryValidateObject(model, ctx, validationResults, true);
            if (model is IValidatableObject) (model as IValidatableObject).Validate(ctx);
            return validationResults;
        }

CRUD tests:

Following the 3 step approach: Arrange, Assert, Act.

Some tips:

You need a builder (DbContextOptionsBuilder), and a context. Your arrange will look something like this:

// Arrange
var builder = new DbContextOptionsBuilder().UseInMemoryDatabase(databaseName: “InMemoryDb_Edit”);
var context = new ApplicationDbContext(builder.Options);
Seed(context);

Create a Seed helper method:

1
2
3
4
5
6
7
8
9
10
11
12
        private void Seed(ApplicationDbContext context)
        {
            var books = new[]
            {
                new Book() { Name = "Name1", Author = "Author1", ISBN = "moo1", Id = 1},
                new Book() { Name = "Name2", Author = "Author2", ISBN = "moo2", Id = 2},
                new Book() { Name = "Name3", Author = "Author3", ISBN = "moo3", Id = 3}
            };

            context.Books.AddRange(books);
            context.SaveChanges();
    }

Create a Teardown helper method:

1
2
3
4
5
6
        private async Task Teardown(ApplicationDbContext context)
        {
            var books = await context.Books.ToListAsync();
            context.Books.RemoveRange(books);
            context.SaveChanges();
         }

Check you can Create, Update and Delete models from a database instance.

Using InMemoryDatabase:

Create example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
        [Fact]
        public async System.Threading.Tasks.Task Create_OnPost_BookShouldBeAddedAsync()
        {
            // Arrange
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>().UseInMemoryDatabase(databaseName: "InMemoryDb_Create");
            var context = new ApplicationDbContext(builder.Options);
            Seed(context); // See above for this Helper Method

            // Act
            var model = new CreateModel(context);

            var book = new Book()
            {
                Name = "NameTest",
                ISBN = "ISBNTest",
                Author = "AuthorTest"
            };

            await model.OnPost(book);

            // Assert
            var books = await context.Books.ToListAsync();
            Assert.Equal(4, books.Count);
            Assert.Matches(books[3].Name, "NameTest");
        }

Read example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        [Fact]
        public async void Index_OnGet_BooksShouldSet()
        {
            // Arrange
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>()
                .UseInMemoryDatabase(databaseName: "InMemoryDb_Index");
            var mockAppDbContext = new ApplicationDbContext(builder.Options);

            Seed(mockAppDbContext);

            var pageModel = new IndexModel(mockAppDbContext);

            // Act
            await pageModel.OnGet();

            // Assert
            var actualMessages = Assert.IsAssignableFrom<List<Book>>(pageModel.Books);
            Assert.Equal(3, actualMessages.Count);

            await Teardown(mockAppDbContext);
}

Update example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        [Fact]
        public async void Edit_OnGet_EditBookEntryIfValid()
        {
            // Arrange
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>().UseInMemoryDatabase(databaseName: "InMemoryDb_Edit");
            var context = new ApplicationDbContext(builder.Options);
            Seed(context);

            // Act
            var editPage = new EditModel(context);
            editPage.OnGet(2);

            editPage.Book.Author = "Test2";
            editPage.Book.ISBN = "Test2";
            editPage.Book.Name = "Test2";

            await editPage.OnPost();

            var books = await context.Books.ToListAsync();

            // Assert
            Assert.Equal(editPage.Book, books[1]);
            Assert.Matches(books[1].Name, "Test2");
            Assert.Matches(books[1].ISBN, "Test2");
            Assert.Matches(books[1].Author, "Test2");

            Assert.Matches(editPage.Message, "Book has been updated successfully");

            await Teardown(context);
        }

Delete example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        [Fact]
        public async void Index_OnPostDelete_BookGetsDeleted()
        {
            // Arrange
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>()
                .UseInMemoryDatabase(databaseName: "InMemoryDb_Index");
            var mockAppDbContext = new ApplicationDbContext(builder.Options);

            Seed(mockAppDbContext);

            var pageModel = new IndexModel(mockAppDbContext);

            // Act
            var deleteBooks = await mockAppDbContext.Books.ToListAsync();
            await pageModel.OnPostDelete(deleteBooks[1].Id);


            var books = await mockAppDbContext.Books.ToListAsync();

            // Assert
            Assert.Equal(2, books.Count);

            Assert.Matches(pageModel.Message, "Book deleted");

            await Teardown(mockAppDbContext);
        }