Skip to content

Commit

Permalink
Improve test from a business point of view (now compares books instea…
Browse files Browse the repository at this point in the history
…d of just counting them), use specification and split builders for better separation of concerns.
  • Loading branch information
GTechene authored and benoit-maurice committed Dec 20, 2023
1 parent 76a577e commit eaa522c
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 64 deletions.
36 changes: 36 additions & 0 deletions tests/BookShop.AcceptanceTests/Builders/BookSpecification.cs
@@ -0,0 +1,36 @@
using BookShop.domain;
using BookShop.domain.Catalog;
using Diverse;

namespace BookShop.AcceptanceTests.Builders;

public class BookSpecification
{
public ISBN Isbn { get; }
public Quantity Quantity { get; }
public Uri PictureUrl { get; }
public int NumberOfPages { get; }
public string Author { get; }
public string Title { get; }
public int NumberOfRatings { get; }
public decimal AverageRating { get; }

public BookSpecification(IFuzz fuzzer)
{
Isbn = fuzzer.GenerateIsbn10();
Title = fuzzer.GenerateSentence(6);
var firstName = fuzzer.GenerateFirstName();
var lastName = fuzzer.GenerateLastName(firstName);
Author = $"{firstName} {lastName}";
NumberOfPages = fuzzer.GenerateInteger(10, 1500);
PictureUrl = new Uri(fuzzer.GenerateStringFromPattern("http://picture-url-for-tests/xxxxxxx.jpg"));
Quantity = new Quantity(fuzzer.GenerateInteger(1, 100));
AverageRating = Math.Round(fuzzer.GeneratePositiveDecimal(0m, 5m), 2);
NumberOfRatings = fuzzer.GenerateInteger(2, 20000);
}

public Book ToBook()
{
return new Book(new BookReference(Isbn, Title, Author, NumberOfPages, PictureUrl), Quantity);
}
}
58 changes: 10 additions & 48 deletions tests/BookShop.AcceptanceTests/Builders/CatalogControllerBuilder.cs
@@ -1,85 +1,47 @@
using BookShop.api.Controllers;
using BookShop.domain;
using BookShop.domain.Catalog;
using BookShop.infra;
using Diverse;
using NSubstitute;

namespace BookShop.AcceptanceTests.Builders;

public class CatalogControllerBuilder
{
private readonly IFuzz _fuzzer = new Fuzzer();
private Book[] _booksInCatalog = Array.Empty<Book>();
private BookSpecification[] _booksInCatalog = Array.Empty<BookSpecification>();

public CatalogControllerBuilder WithRandomBooks(int numberOfBooksToGenerate)
public CatalogControllerBuilder WithBooks(params BookSpecification[] books)
{
_booksInCatalog = Enumerable
.Range(0, numberOfBooksToGenerate)
.Select(_ => {
var randomIsbn = new ISBN.ISBN10(_fuzzer.GenerateInteger(1, 100), _fuzzer.GenerateInteger(1, 10000), _fuzzer.GenerateInteger(1, 1000), _fuzzer.GenerateInteger(1, 10));
var title = _fuzzer.GenerateSentence(6);
var firstName = _fuzzer.GenerateFirstName();
var lastName = _fuzzer.GenerateLastName(firstName);
var authorName = $"{firstName} {lastName}";
var numberOfPages = _fuzzer.GenerateInteger(10, 1500);
var pictureUrl = new Uri(_fuzzer.GenerateStringFromPattern("http://picture-url-for-tests/xxxxxxx.jpg"));
var quantityInStock = _fuzzer.GenerateInteger(1, 100);
return new Book(new BookReference(randomIsbn, title, authorName, numberOfPages, pictureUrl), quantityInStock);
})
.ToArray();

_booksInCatalog = books;
return this;
}

public CatalogController Build()
{
var bookMetadataProvider = StubBookMetadataProvider();
var inventoryProvider = StubInventoryProvider();

var catalogProvider = new CatalogService(bookMetadataProvider, inventoryProvider);
var catalogServiceBuilder = new CatalogServiceBuilder(_booksInCatalog);
var catalogProvider = catalogServiceBuilder.Build();

var bookPriceProvider = new BookPriceRepository();
var bookMetadataProvider = StubBookMetadataProvider();
var bookAdvisorHttpClient = StubBookAdvisorHttpClient();

return new CatalogController(catalogProvider, bookPriceProvider, bookMetadataProvider, bookAdvisorHttpClient);
}

private IProvideBookMetadata StubBookMetadataProvider()
{

var bookMetadataProvider = Substitute.For<IProvideBookMetadata>();

foreach (var book in _booksInCatalog)
{
bookMetadataProvider.Get(book.Reference.Id)
.Returns(book.Reference);
bookMetadataProvider.Get(book.Isbn)
.Returns(new BookReference(book.Isbn, book.Title, book.Author, book.NumberOfPages, book.PictureUrl));
}

bookMetadataProvider.Get()
.Returns(_booksInCatalog.Select(book => book.Reference).ToList());

return bookMetadataProvider;
}

private IProvideInventory StubInventoryProvider()
{
var inventoryProvider = Substitute.For<IProvideInventory>();
inventoryProvider.Get(Arg.Any<IEnumerable<BookReference>>())
.Returns(callInfo =>
{
var requestedBookReferences = callInfo.Arg<IEnumerable<BookReference>>();
return _booksInCatalog.Where(book => requestedBookReferences.Contains(book.Reference));
});

return inventoryProvider;
}

private BookAdvisorHttpClient StubBookAdvisorHttpClient()
{
return new BookAdvisorHttpClient(new HttpClient(new MockBookAdvisorHttpHandler(_fuzzer))
return new BookAdvisorHttpClient(new HttpClient(new MockBookAdvisorHttpHandler(_booksInCatalog))
{
BaseAddress = new Uri("http://fake-base-address-for-tests")
});
Expand Down
30 changes: 22 additions & 8 deletions tests/BookShop.AcceptanceTests/Builders/CatalogControllerShould.cs
@@ -1,3 +1,4 @@
using BookShop.shared;
using Diverse;
using NFluent;
using Xunit;
Expand All @@ -7,22 +8,35 @@ namespace BookShop.AcceptanceTests.Builders;

public class CatalogControllerShould
{
public CatalogControllerShould(ITestOutputHelper output)
{
Fuzzer.Log = output.WriteLine;
}

[Fact]
public async Task List_all_books_when_called_on_GetCatalog()
{
const int numberOfBooksInCatalog = 3;
var fuzzer = new Fuzzer();

var books = new BookSpecification[]
{
new(fuzzer),
new(fuzzer),
new(fuzzer)
};

var controller = new CatalogControllerBuilder()
.WithRandomBooks(numberOfBooksInCatalog)
.WithBooks(books)
.Build();

var catalogResponse = await controller.GetCatalog("EUR");

Check.That(catalogResponse.Books).HasSize(books.Length);

Check.That(catalogResponse.Books).HasSize(numberOfBooksInCatalog);
// This because of the sloppy implementation of our BookPriceRepository :)
var uniqueBookPrice = new Price(8m, "EUR");

var expectedResponse = books.Select(book => new BookResponse(book.Isbn.ToString(), book.Title, book.Author, book.NumberOfPages, new RatingsResponse(book.AverageRating, book.NumberOfRatings), book.PictureUrl.ToString(), book.Quantity.Amount, uniqueBookPrice));
Check.That(catalogResponse.Books).IsEquivalentTo(expectedResponse);
}

public CatalogControllerShould(ITestOutputHelper output)
{
Fuzzer.Log = output.WriteLine;
}
}
41 changes: 41 additions & 0 deletions tests/BookShop.AcceptanceTests/Builders/CatalogServiceBuilder.cs
@@ -0,0 +1,41 @@
using BookShop.domain.Catalog;
using NSubstitute;

namespace BookShop.AcceptanceTests.Builders;

public class CatalogServiceBuilder(BookSpecification[] booksInCatalog)
{
public CatalogService Build()
{
var bookMetadataProvider = StubBookMetadataProvider();
var inventoryProvider = StubInventoryProvider();

return new CatalogService(bookMetadataProvider, inventoryProvider);
}

private IProvideBookMetadata StubBookMetadataProvider()
{
var bookMetadataProvider = Substitute.For<IProvideBookMetadata>();

bookMetadataProvider.Get()
.Returns(booksInCatalog.Select(book => new BookReference(book.Isbn, book.Title, book.Author, book.NumberOfPages, book.PictureUrl)).ToList());

return bookMetadataProvider;
}

private IProvideInventory StubInventoryProvider()
{
var inventoryProvider = Substitute.For<IProvideInventory>();
inventoryProvider.Get(Arg.Any<IEnumerable<BookReference>>())
.Returns(callInfo =>
{
var requestedBookReferences = callInfo.Arg<IEnumerable<BookReference>>();
return booksInCatalog
.Where(book => requestedBookReferences.Contains(new BookReference(book.Isbn, book.Title, book.Author, book.NumberOfPages, book.PictureUrl)))
.Select(book => book.ToBook());
});

return inventoryProvider;
}
}
11 changes: 11 additions & 0 deletions tests/BookShop.AcceptanceTests/Builders/FuzzerExtensions.cs
@@ -0,0 +1,11 @@
using BookShop.domain;
using Diverse;

namespace BookShop.AcceptanceTests.Builders;
public static class FuzzerExtensions
{
public static ISBN.ISBN10 GenerateIsbn10(this IFuzz fuzzer)
{
return new ISBN.ISBN10(fuzzer.GenerateInteger(1, 100), fuzzer.GenerateInteger(1, 10000), fuzzer.GenerateInteger(1, 1000), fuzzer.GenerateInteger(1, 10));
}
}
@@ -1,23 +1,26 @@
using System.Net;
using System.Net.Http.Json;
using BookShop.domain;
using BookShop.shared;
using Diverse;

namespace BookShop.AcceptanceTests.Builders;

internal class MockBookAdvisorHttpHandler(IFuzzNumbers fuzzer) : HttpMessageHandler
internal class MockBookAdvisorHttpHandler(BookSpecification[] books) : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri is not null && request.RequestUri.AbsolutePath.StartsWith("/reviews/ratings/"))
{
var rating = Math.Round(fuzzer.GeneratePositiveDecimal(0m, 5m), 2);
var numberOfRatings = fuzzer.GenerateInteger(2, 20000);

return new HttpResponseMessage(HttpStatusCode.OK)
var path = request.RequestUri.AbsolutePath;
var requestedIsbn = path.Substring(path.LastIndexOf('/') + 1);
var matchingBook = books.SingleOrDefault(book => book.Isbn == ISBN.Parse(requestedIsbn));
if (matchingBook != null)
{
Content = JsonContent.Create(new RatingsResponse(rating, numberOfRatings))
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = JsonContent.Create(new RatingsResponse(matchingBook.AverageRating, matchingBook.NumberOfRatings))
};
}
}

return new HttpResponseMessage(HttpStatusCode.NotFound);
Expand Down

0 comments on commit eaa522c

Please sign in to comment.