Posts da Tag: Flurl - Blog da TreinaWeb

C#

C# – Testando requisições com Flurl

No meu artigo passado, demostrei a facilidade do uso da biblioteca Flurl no consumo de dados de uma API. Entretanto o ponto de maior orgulho desta biblioteca é a facilidade em testar as requisições criadas com ela.

Testes de unidades em requisições HTTP

Testar classes que fazem requisições HTTP pode ser um ponto complexo do projeto. Ele precisa ser desenvolvido de forma que aceite um Mock ou fazer uso de uma API de testes.

Uma API de testes pode ser uma boa escolha, mas caso ocorra algo com a mesma, os testes do projeto irão falhar. Enquanto o problema com a API não for resolvido, estes testes não poderão ser realizados.

Então, o mais comum é o uso de Mock, que deve ser implementado de acordo com a biblioteca de requisições implementada, já que a classe HttpClient não pode ser “mockada”. Ela não define interfaces que facilitariam este processo.

Dentre as bibliotecas de requisição que permitem o uso de Mock, a Flurl é a que torna este processo o mais transparente e simples.

Definindo testes de unidade com Flurl

Para exemplificar, serão implementados testes de unidade no projeto apresentado no meu artigo anterior. Nele vimos a implementação do Flurl na camada repository do projeto, assim, os testes de unidade mostrados aqui serão realizados apenas nesta camada.

Os testes podem ser feito graças a classe HttpTest da biblioteca. Recomenda-se que ela seja declarada em um bloco using:

using (var httpTest = new HttpTest()) {
    // Realizar as requisições aqui
}

Desta forma, todas as requisições realizadas dentro deste bloco serão interceptadas pela classe e com isso, poderão ser “mockadas”:

public async Task TestListProducts()
{
    var repository = new ProductRepository();
    using (var httpTest = new HttpTest()) {
        // arrange
        httpTest.RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}, {\"id\":\"213drwa3\",\"name\":\"Teclado\", \"quantity\":20, \"price\": 149.9}]");

        // act
        await repository.FindAll();

        // assert
        httpTest
            .ShouldHaveCalled("http://localhost:3002/api/products")
            .WithVerb(HttpMethod.Get);
    }
}

Acima, é definido o mock:

httpTest.RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}, {\"id\":\"213drwa3\",\"name\":\"Teclado\", \"quantity\":20, \"price\": 149.9}]");

Ou seja, não importa qual requisição, será retornado este conteúdo.

Em seguida a requisição é realizada:

await repository.FindAll();

E é verificado se ela foi feita para a API via GET:

httpTest
    .ShouldHaveCalled("http://localhost:3002/api/products")
    .WithVerb(HttpMethod.Get);

Ao executar o teste, ele irá passar:

asciicast

Definindo critérios para as requisições interceptadas

No exemplo anterior todas as requisições realizadas serão interceptadas pela classe HttpTest. Entretanto, é possível refinar isso:

public async Task TestCreateProducts()
{
    var repository = new ProductRepository();
    using (var httpTest = new HttpTest()) {
        // arrange
        httpTest
                .ForCallsTo("http://localhost:3002/*", "https://api.com/*")
                .WithVerb(HttpMethod.Post)
                .RespondWith("[{\"id\":\"12d3d23\",\"name\":\"Mouse\", \"quantity\":10, \"price\": 99.9}", 201);

        var product = new Product {
            Name = "Mouse",
            Quantity = 10,
            Price = 99.9
        };

        // act
        await repository.Add(product);

        // assert
        httpTest
            .ShouldHaveCalled("*/api/products")
            .WithRequestBody("{\"Id\":*,\"Name\":\"Mouse\",\"Quantity\":10,\"Price\":99.9}")
            .WithVerb(HttpMethod.Post);
    }
}

Note que é definido as APIs que serão interceptadas e o verbo HTTP:

httpTest
        .ForCallsTo("*localhost:3002/*", "*api.*.com/*")
        .WithVerb(HttpMethod.Post)

Qualquer requisição que não se enquadre nestes critérios será ignorada pela HttpTest.

No assert, se verifica apenas o endpoint:

httpTest
    .ShouldHaveCalled("*/api/products")

Não é necessário informar o servidor, porque os aceitos já estão definidos no critério especificado. Também note que é utilizado curingas (*). Eles podem ser implementados nos parâmetros de todos os métodos da classe.

Caso o teste seja executado, este segundo também passará:

asciicast

Conclusão

Adicionar testes em um projeto é uma boa prática que todos devem adotar. Ao se trabalhar com requisições, este processo pode ser facilitado ao adotar a biblioteca Flurl. Assim, caso esteja trabalhando com requisições e necessite implementar testes, não deixe de verificar esta biblioteca.

Neste artigo não foram abordados todos os métodos da classe HttpTest. Como vários são úteis, não deixe de vê-los na documentação da mesma.

Então é isso, por hoje é só 🙂


.NET Core ASP .NET C#

C# – Consumindo APIs com Flurl

Com a diversificação do acesso, está se tornando um padrão a criação de back-end, APIs, que posteriormente serão consumidas por outras aplicações. Por isso, atualmente é imprescindível saber realizar este procedimento. Felizmente, no C#, a biblioteca Flurl facilita, e muito, este processo.

Flurl

Criada por Todd Menier, Flurl é uma biblioteca open source para .NET. Ela se define como um builder de URL, moderno, assíncrono, fluent, portável, testável, entre outras buzzword e uma biblioteca de requisições HTTP.

De forma simples, ela nos permite criar requisições HTTP que são facilmente testáveis. Entretanto, não abordaremos este detalhe aqui. Neste arquivo, veremos apenas a criação de requisições.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Aplicação base

Para exemplificar o uso da biblioteca, irei utilizar como base a aplicação demonstrada no artigo da biblioteca RepoDB e o pacote Tw Dev Server do Akira Hanashiro.

Consumindo dados da API

No momento, ao executar a aplicação, os produtos são listados do banco:

Iremos alterá-la pra que esses dados sejam obtidos pela API.

Por esta aplicação adotar o padrão repository, as principais alterações serão realizadas nesta camada. As demais sofrerão apenas adaptações pontuais.

A primeira coisa à ser feita é adicionar a biblioteca Flurl:

dotnet add package Flurl.Http

Por ser uma biblioteca fluent, seus métodos são de extensão. Assim, inicialmente iremos definir a url:

 const string url = "http://localhost:3002/api/products";

Em seguida iremos alterar o método FindAll, que no momento tem o conteúdo abaixo:

public IEnumerable<Product> FindAll()
{
    return QueryAll();
}

A obtenção de dados é feita através de uma requisição GET. Como os dados da nossa API são retornados como JSON, a Flurl possui o método de extensão GetJsonAsync que já cria esta requisição e parseia os dados:

public async Task<IEnumerable<Product>> FindAll()
{
    return await url.GetJsonAsync<List<Product>>();
}

Como GetJsonAsync é assíncrono, note que foi necessário definir o FindAll()como assíncrono. Com isso, será necessário modificar a interface:

Task<IEnumerable<T>> FindAll();

E o controller:

public async Task<ActionResult> Index()
{
    return View((await productRepository.FindAll()).ToList());
}

Também será necessário modificar o model, porque os ids criados pela API são GUID:

public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}

Como a API não possui nenhum dado, por enquanto a listagem não mostra nada:

Listagem de produtos com nenhum item

Enviando dados para a API

Com a listagem pronta, vamos enviar dados para a API. Isso é feito via uma requisição POST. Assim, como a GET, a biblioteca possui o método de extensão PostJsonAsync que já facilita a criação desta requisição:

public async Task Add(Product item)
{
    await url.PostJsonAsync(item);
}

Também é necessário modificar a interface:

Task Add(T item);

E o controller:

public async Task<ActionResult> Create([Bind("Id,Name,Quantity,Price")] Product product)
{
    if (ModelState.IsValid)
    {
        await productRepository.Add(product);
        return RedirectToAction("Index");
    }

    return View(product);
}

Agora podemos adicionar dados na API e consumi-los:

Listagem de produtos com um item

Atualizando dados da API

O processo de atualização ocorre em duas etapas. Inicialmente é necessário obter os dados do registro que será atualizado. Para isso, deve ser feita uma requisição GET, passando o id do registro:

public async Task<Product> FindByID(string id)
{
    return await url
                .SetQueryParams(new { id = id })
                .GetJsonAsync<Product>();
}

Note que estamos utilizando o método SetQueryParams passar o id via querystring. No final, teremos uma URL no seguinte formato: http://localhost:3002/api/products?id=<valor id>.

Assim como antes, também é necessário mudar a interface:

Task<T> FindByID(string id);

E o controller:

public async Task<ActionResult> Edit(string id)
{
    if (id == null)
    {
        return StatusCode(StatusCodes.Status400BadRequest);
    }
    Product product = await productRepository.FindByID(id);
    if (product == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    return View(product);
}

Aproveite e altere o método de detalhes:

public async Task<ActionResult> Details(string id)
{
    if (id == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    Product product = await productRepository.FindByID(id);
    if (product == null)
    {
        return StatusCode(StatusCodes.Status404NotFound);
    }
    return View(product);
}

Agora ao clicar no link de edição (ou detalhes), os dados do produto serão mostrados:

Tela de edição mostrando os detalhes de um produto

A atualização dos dados é realizada via uma requisição PUT e como deve imaginar, a Flurl também fornece um método de extensão que facilita a implementação desta requisição:

public async Task Update(Product item)
{
    await url
            .SetQueryParams(new { id = item.Id })
            .PutJsonAsync(item);
}

Note que também é necessário passar o ID via querystring, pois esta é uma especificação da API.

Não se esqueça de mudar a interface:

Task Update(T item);

E o controller:

public async Task<ActionResult> Edit([Bind("Id,Name,Quantity,Price")] Product product)
{
    if (ModelState.IsValid)
    {
        await productRepository.Update(product);
        return RedirectToAction("Index");
    }
    return View(product);
}

Agora, poderemos alterar os nossos registros:

Listagem de produtos exibindo um produto alterado

Excluindo dados da API

Para finalizar, falta implementar apenas a exclusão dos dados. Isso é feito via uma requisição DELETE, que pode ser implementada via o método de extensão DeleteAsync:

public async Task Remove(string id)
{
    await url
            .SetQueryParams(new { id = id })
            .DeleteAsync();
}

Não se esqueça que é necessário alterar a interface:

Task Remove(string id);

E o controller:

public async Task<ActionResult> DeleteConfirmed(string id)
{
    await productRepository.Remove(id);
    return RedirectToAction("Index");
}

Ao remover o único registro da nossa lista, ela voltará a ficar vazia:

Listagem de produtos com nenhum item

C# - Algoritmos
Curso de C# - Algoritmos
CONHEÇA O CURSO

Conclusão

Note que com poucas alterações, conseguimos alterar a fonte de dados da aplicação para uma API. E esta comunicação com a API foi facilitada graças a biblioteca Flurl.

Esta biblioteca fornece uma grande gama de recursos e opções que conheceremos em artigos futuros. Entretanto, caso necessite trabalhar com API no .NET, não deixe de dar uma olhada na sua documentação. Tenho certeza que ela irá facilitar, e muito, o seu trabalho.