Compartilhar via



Maio de 2016

Volume 31 - Número 5

ASP.NET - Como escrever um código claro no ASP.NET Core com Injeção de dependência

Por Steve Smith 

O ASP.NET Core 1.0 é uma versão totalmente reescrita do ASP.NET, e um dos principais objetivos desta nova estrutura é ter um design mais modular. Ou seja, os aplicativos devem ser capazes de utilizar somente as partes da estrutura de que precisam, com a estrutura fornecendo as dependências conforme elas forem solicitadas. Além disso, os desenvolvedores que compilam aplicativos usando o ASP.NET Core devem ser capazes de utilizar essa mesma funcionalidade para manter seus aplicativos modulares e acoplados de forma mais livre. Com o ASP.NET MVC, a equipe do ASP.NET aprimorou muito o suporte da estrutura para a escrita de código com acoplamento mais livre, mas, ainda assim, foi muito fácil cair na armadilha do acoplamento rígido, especialmente nas classes de controlador.

Acoplamento rígido

O acoplamento rígido funciona bem para software de demonstração. Se analisar um aplicativo de exemplo comum que mostra como criar sites no ASP.NET MCV (versões 3 a 5), será muito provável que você encontre código semelhante a este (da classe DinnersController do exemplo de MVC 4 do NerdDinner):

private NerdDinnerContext db = new NerdDinnerContext();
private const int PageSize = 25;
public ActionResult Index(int? page)
{
  int pageIndex = page ?? 1;
  var dinners = db.Dinners
    .Where(d => d.EventDate >= DateTime.Now).OrderBy(d => d.EventDate);
  return View(dinners.ToPagedList(pageIndex, PageSize));
}

Com esse tipo de código, é muito difícil realizar testes de unidade, poque NerdDinnerContext é criado como parte da construção da classe e requer um banco de dados com o qual se conectar. Não é de surpreender que, frequentemente, esses aplicativos de demonstração não incluam testes de unidade. No entanto, seu aplicativo pode ser beneficiado por alguns testes de unidade, ainda que você não vá fazer test drive de seu desenvolvimento, de modo que seria melhor escrever o código de forma que ele pudesse ser testado. Além disso, esse código viola o princípio DRY (Não se Repita), porque toda classe de controlador que realiza qualquer acesso a dados tem o mesmo código para criar um contexto de banco de dados de EF (Entity Framework). Isso faz com que futuras mudanças sejam mais caras e propensas a erros, especialmente conforme o aplicativo for crescendo com o passar do tempo.

Ao analisar um código para avaliar seu acoplamento, lembre-se da expressão "new is glue" ("'new' é uma cola"). Ou seja, sempre que vir a palavra-chave “new” instanciando uma classe, perceba que você está "colando" sua implementação no código de implementação específico em questão. O Princípio de Inversão de Dependência (bit.ly/DI-Principle) afirma que: “Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.” Neste exemplo, os detalhes de como o controlador reúne os dados para transmitir para a exibição dependem dos detalhes relacionados a como obter esses dados — no caso, o EF.

Além da palavra-chave "new", “static cling” é outra fonte de acoplagem rígida que dificulta o teste e a manutenção de aplicativos. No exemplo anterior, há uma dependência do relógio do sistema do computador em execução, na forma de uma chamada para DateTime.Now. Esse acoplamento dificultaria a criação de um conjunto de jantares de teste para serem usados em testes de unidade, porque suas propriedades EventDate precisariam ser definidas em relação à configuração atual do relógio. Esse acoplamento poderia ser removido do método de várias formas, sendo a mais simples delas deixar que qualquer nova abstração que retorne os jantares se preocupe com ele, de modo que ele deixa de ser parte do método. Você também pode fazer do valor um parâmetro, de modo que o método pode retornar todos os jantares após um parâmetro DateTime, em vez de sempre usar DateTime.Now. Por fim, poderíamos criar uma abstração para a hora atual e referenciar a hora atual por meio dessa abstração. Esta pode ser uma boa abordagem se o aplicativo referenciar DateTime.Now com frequência. (Também vale notar que, como esses jantares provavelmente acontecem em diversos fusos horários, o tipo DateTimeOffset pode ser uma escolha melhor para um aplicativo real).

Seja honesto

Outro problema relacionado à facilidade de manutenção de um código como esse é que ele não é honesto com seus colaboradores. Você deve evitar escrever classes que podem ser instanciadas em estados inválidos, pois frequentemente elas são fontes de erros. Sendo assim, tudo de que sua classe precisa para realizar suas tarefas deve ser fornecido por meio de seu construtor. Como afirma o Princípio de Dependências Explícitas(bit.ly/ED-Principle), “Métodos e classes devem exigir explicitamente quaisquer objetos colaboradores de que precisarem para funcionar corretamente.” A classe DinnersController tem somente um construtor padrão, o que deixa implícito que ela não deveria precisar de nenhum colaborador para funcionar corretamente. Mas o que acontece se testarmos isso? O que o código faria se você o executasse de um novo aplicativo de console que referencia o projeto de MVC?

var controller = new DinnersController();
var result = controller.Index(1);

A primeira coisa que falha neste caso é a tentativa de instanciar o contexto de EF. O código devolve uma InvalidOperationException: “Não foi possível encontrar uma cadeia de conexão chamada ‘NerdDinnerContext’ no arquivo de configuração do aplicativo.” Fui enganado! A classe precisa de mais para funcionar do que aquilo que o construtor afirma! Se a classe precisa de uma maneira de acessar coleções de instância de Jantar, ela deve solicitar isso por meio de seu construtor (ou como parâmetros em seus métodos).

Injeção de dependência

DI (Injeção de dependência) se refere à técnica de transmitir as dependências de uma classe ou método como parâmetros, em vez de codificar essas relações por meio de chamadas novas ou estáticas. Trata-se de uma técnica cada vez mais comum no desenvolvimento de .NET devido ao desacoplamento que confere aos aplicativos que a empregam. Versões anteriores do ASP.NET não usavam a DI e, embora o ASP.NET MVC e a API Web tenham trazido progressos relativos ao seu suporte, nenhum dos dois conferia suporte completo no produto, incluindo um contêiner para gerenciar as dependências e os ciclos de vida de seis objetos. Com o ASP.NET Core 1.0, a DI não só tem suporte integrado como é usada pelo próprio produto.

O ASP.NET Core não só dá suporte à DI, ele também inclui um contêiner de DI — também chamado de contêiner de IoC (Inversão de controle) ou contêiner de serviços. Cada aplicativo ASP.NET Core configura suas dependências usando esse contêiner no método ConfigureServices da classe Startup. O contêiner dá o suporte básico necessário, mas pode ser substituído por uma implantação personalizada se desejado. Além disso, o EF Core também tem suporte interno para DI, de modo que configurá-lo em um aplicativo ASP.NET Core é tão simples quanto chamar um método de extensão. Eu criei uma variação do NerdDinner, chamada GeekDinner, para este artigo. O EF Core é configurado desta forma:

public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFramework()
    .AddSqlServer()
    .AddDbContext<GeekDinnerDbContext>(options =>
      options.UseSqlServer(ConnectionString));
  services.AddMvc();
}

Tendo isso, é bem simples usar a DI para solicitar uma instância de GeekDinnerDbContext de uma classe de controlador como DinnersController:

public class DinnersController : Controller
{
  private readonly GeekDinnerDbContext _dbContext;
  public DinnersController(GeekDinnerDbContext dbContext)
  {
    _dbContext = dbContext;
  }
  public IActionResult Index()
  {
    return View(_dbContext.Dinners.ToList());
  }
}

Observe que não há nenhuma instância da palavra-chave "new". As dependências de que o controlador precisa são todas transmitidas por meio de seu construtor e o contêiner de DI do ASP.NET cuida disso para mim. Enquanto estou concentrado em escrever o aplicativo, eu não preciso me preocupar com a dinâmica envolvida em cumprir as dependências que minhas classes solicitam por meio de seus construtores. É claro que, se quiser, eu posso personalizar esse comportamento, até mesmo substituindo completamente o contêiner padrão por outra implementação. Como minha classe de controlador agora segue o princípio das dependências explícitas, eu sei que, para ele funcionar, preciso fornecer uma instância de um GeekDinnerDbContext. Eu posso, configurando um pouco o DbContext, instanciar o controlador isoladamente, como demonstrado neste aplicativo de console:

var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(Startup.ConnectionString);
var dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
var controller = new DinnersController(dbContext);
var result = (ViewResult) controller.Index();

Há um pouco mais de trabalho envolvido na construção de um DbContext do EF Core do que em um do EF6, que precisou apenas de uma cadeia de conexão. Isso acontece porque, assim como o ASP.NET Core, o EF Core foi criado para ser mais modular. Normalmente você não precisaria lidar diretamente com o DbContextOptionsBuilder, porque ele é usado nos bastidores quando você configura o EF por meio de métodos de extensão como AddEntityFramework e AddSqlServer.

E é possível testá-lo?

Testar seu aplicativo manualmente é importante — é bom poder executá-lo para ver se ele de fato é executado e produz o resultado esperado. Mas ter que fazer isso toda vez que você faz uma alteração é perda de tempo. Um dos grandes benefícios de um aplicativo com acoplamento mais livre é que ele tende a ser mais propício para testes de unidade do que aplicativos com acoplamento mais rígido. E melhor ainda, o ASP.NET Core e o EF Core são muito mais fáceis de testar do que seus antecessores. Para começar, vou escrever um teste simples diretamente para o controlador transmitindo um DbContext que foi configurado para usar um repositório na memória. Vou configurar o GeekDinnerDbContext usando o parâmetro DbContextOptions que ele expõe por meio de seu construtor como parte do código de configuração do meu teste:

var optionsBuilder = new DbContextOptionsBuilder<GeekDinnerDbContext>();
optionsBuilder.UseInMemoryDatabase();
_dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
// Add sample data
_dbContext.Dinners.Add(new Dinner() { Title = "Title 1" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 2" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 3" });
_dbContext.SaveChanges();

Com isso configurado em minha classe de teste, é fácil escrever um teste mostrando que os dados corretos são retornados no Modelo do ViewResult:

[Fact]
public void ReturnsDinnersInViewModel()
{
  var controller = new OriginalDinnersController(_dbContext);
  var result = controller.Index();
  var viewResult = Assert.IsType<ViewResult>(result);
  var viewModel = Assert.IsType<IEnumerable<Dinner>>(
    viewResult.ViewData.Model).ToList();
  Assert.Equal(1, viewModel.Count(d => d.Title == "Title 1"));
  Assert.Equal(3, viewModel.Count);
}

É claro que ainda não há muita lógica a ser testada aqui, de modo que o teste na verdade não está testando muita coisa. Críticos argumentariam que não se trata de um teste muito valioso, e eu concordaria com eles. No entanto, é um ponto inicial para quando houver mais lógica, e neste caso logo haverá. Mas antes, embora o EF Core possa dar suporte ao teste de unidade com sua opção na memória, ainda vou evitar o acoplamento direto com o EF em meu controlador. Não há motivos para acoplar aspectos da interface do usuário com aspectos da infraestrutura de acesso a dados - na verdade, isto viola outro princípio, o da Separação de Aspectos.

Não dependa daquilo que você não usa

O Princípio da Segregação da Interface (bit.ly/LS-Principle) afirma que as classes devem depender apenas de funcionalidades que elas de fato usam. No caso do novo DinnersController habilitado para DI, ele ainda é dependente de todo o DbContext. Em vez de unir a implementação do controlador ao EF, poderia ser usada uma abstração que fornecesse a funcionalidade necessária (e pouco ou nada além disso).

Do que esse método de ação realmente precisa para funcionar? Com certeza, não precisa de todo o DbContext. Ele nem mesmo precisa de acesso a toda a propriedade Dinners do contexto. Ele precisa apenas da capacidade de exibir as instâncias de Dinner da página correta. A abstração .NET mais simples que representa isso é IEnumerable<Dinner>. Assim, eu definirei uma interface que simplesmente retorna IEnumerable<Dinner> e que satisfará os requisitos do método Index (ou a maioria deles):

public interface IDinnerRepository
{
  IEnumerable<Dinner> List();
}

Estou chamando isto de repositório porque segue aquele padrão: Ele abstrai o acesso a dados por trás de uma interface semelhante a uma coleção. Se por algum motivo não gostar do nome ou do padrão do repositório, você poderá chamá-lo de IGetDinners ou IDinnerService ou de qualquer nome que preferir (meu revisor técnico sugere ICanHasDinner). Independentemente do nome que você der ao tipo, ele terá a mesma finalidade.

Tendo isso, agora eu posso ajustar o DinnersController para aceitar um IDinnerRepository como parâmetro de construtor, em vez de um GeekDinnerDbContext, e chamar o método List em vez de acessar o Dinners DbSet diretamente:

private readonly IDinnerRepository _dinnerRepository;
public DinnersController(IDinnerRepository dinnerRepository)
{
  _dinnerRepository = dinnerRepository;
}
public IActionResult Index()
{
  return View(_dinnerRepository.List());
}

Neste ponto, você pode compilar e executar seu aplicativo Web, mas encontrará uma exceção se navegar até /Dinners: Invalid­OperationException: Não foi possível resolver o serviço para o tipo “Geek­Dinner.Core.Interfaces.IdinnerRepository” ao tentar ativar GeekDinner.Controllers.DinnersController. Ainda não implementei a interface e, assim que o fizer, também precisarei configurar minha implementação para ser usada quando a DI atender solicitações de IDinnerRepository. A implementação da interface é trivial:

public class DinnerRepository : IDinnerRepository
{
  private readonly GeekDinnerDbContext _dbContext;
  public DinnerRepository(GeekDinnerDbContext dbContext)
  {
    _dbContext = dbContext;
  }
  public IEnumerable<Dinner> List()
  {
    return _dbContext.Dinners;
  }
}

Observe que não há problema algum em acoplar uma implementação de repositório diretamente ao EF. Se precisar tirar o EF, eu simplesmente criarei uma nova implementação dessa interface. A classe desta implementação faz parte da infraestrutura do meu aplicativo, que é o único lugar do aplicativo em que minhas classes dependem de implementações específicas.

Para configurar o ASP.NET Core para injetar a implementação correta quando as classes solicitarem um IDinnerRepository, eu preciso adicionar a seguinte linha de código ao método ConfigureServices mostrado anteriormente:

services.AddScoped<IDinnerRepository, DinnerRepository>();

Essa instrução instrui o contêiner de DI do ASP.NET Core a usar uma instância de DinnerRepository sempre que o contêiner estiver resolvendo um tipo que depende de uma instância de IDinnerRepository. Scoped significa que uma instância será usada para cada solicitação da Web que o ASP.NET manipular. Também podem ser adicionados tempos de vida do tipo Transient ou Singleton. Neste caso, Scoped é adequado porque meu DinnerRepository depende de um DbContext, que também usa o tempo de vida Scoped. Veja um resumo dos tempos de vida de objeto disponíveis:

  • Transitório: uma nova instância do tipo é usada sempre que ele é solicitado.
  • Com escopo: uma nova instância do tipo é criada na primeira vez em que for solicitada em uma dada solicitação HTTP e é usada novamente para todos os tipos subsequentes resolvidos durante essa solicitação HTTP.
  • Singleton: uma única instância do tipo é criada uma vez e é usada por todas as solicitações subsequentes desse tipo.

O contêiner interno dá suporte a diversas maneiras de construir os tipos que ele fornecerá. O caso mais comum é simplesmente fornecer um tipo ao contêiner e ele tentará instanciar esse tipo, fornecendo quaisquer dependências que o tipo demandar. Você também pode fornecer uma expressão lambda para construir o tipo ou, para um tempo de vida Singleton, pode fornecer a instância totalmente construída em ConfigureServices quando registrá-la.

Com a injeção de dependência configurada, o aplicativo é executado da mesma forma que antes. Agora, como a Figura 1 mostra, posso testá-lo com essa nova abstração em vigor, usando uma implementação falsa ou fictícia da interface do IDinner­Repository em vez de depender do EF diretamente em meu código de teste.

Figura 1 Testando DinnersController usando um objeto fictício

public class DinnersControllerIndex
{
  private List<Dinner> GetTestDinnerCollection()
  {
    return new List<Dinner>()
    {
      new Dinner() {Title = "Test Dinner 1" },
      new Dinner() {Title = "Test Dinner 2" },
    };
  }
  [Fact]
  public void ReturnsDinnersInViewModel()
  {
    var mockRepository = new Mock<IDinnerRepository>();
    mockRepository.Setup(r =>
      r.List()).Returns(GetTestDinnerCollection());
    var controller = new DinnersController(mockRepository.Object, null);
    var result = controller.Index();
    var viewResult = Assert.IsType<ViewResult>(result);
    var viewModel = Assert.IsType<IEnumerable<Dinner>>(
      viewResult.ViewData.Model).ToList();
    Assert.Equal("Test Dinner 1", viewModel.First().Title);
    Assert.Equal(2, viewModel.Count);
  }
}

Este teste funciona independentemente da origem das instâncias de Dinner. Você pode reescrever o código de acesso a dados para usar outro banco de dados, o Armazenamento de Tabelas do Azure ou arquivos XML e o controlador funcionaria da mesma forma. É claro que neste caso não estamos fazendo grande coisa, então você pode estar se perguntando...

E quanto à lógica de verdade?

Até agora, não implementei nenhuma lógica de negócios de verdade, apenas métodos simples que retornam coleções de dados simples. O valor real dos testes está em quando você tem lógica e casos especiais e precisa estar certo de que eles se comportarão da forma desejada. Para demonstrar isso, vou adicionar alguns requisitos ao meu site GeekDinner. O site apresentará uma API que permitirá que qualquer pessoa confirme presença em um jantar. No entanto, os jantares terão uma capacidade máxima opcional e as confirmações não podem ultrapassar essa capacidade. Os usuários que desejarem confirmar presença além da capacidade máxima deverão ser adicionados a uma lista de espera. Por fim, os jantares podem especificar um prazo até o qual as confirmações deverão ser recebidas, relativo ao seu horário de início, e após esse prazo eles deixarão de aceitar confirmações.

Eu poderia codificar toda essa lógica em uma ação, mas acredito que seja responsabilidade demais colocar tudo em um método, especialmente um método de interface do usuário que deveria estar focada em questões relacionadas à interface do usuário e não na lógica de negócios. O controlador deve confirmar que as entradas que ele recebe são válidas e deve garantir que as respostas que retorna sejam adequadas para o cliente. Decisões que vão além disso, e especialmente a lógica de negócios, não devem estar nos controladores.

O melhor lugar para manter a lógica de negócios é no modelo de domínio do aplicativo, que não deve depender de questões de infraestrutura (como bancos de dados ou interfaces do usuário). O que faz mais sentido é que a classe Dinner gerencie as questões relacionadas às confirmações de presença descritas nos requisitos, porque ela armazenará a capacidade máxima do jantar e saberá quantas confirmações foram feitas até o momento. No entanto, parte da lógica também depende de quando a confirmação ocorre - se o prazo já foi ultrapassado ou não -, de modo que o método também precisa ter acesso à hora atual.

Eu poderia simplesmente usar DateTime.Now, mas isso dificultaria testar a lógica e acoplaria meu modelo de domínio ao relógio do sistema. Outra opção é usar uma abstração IDateTime e injetá-la na entidade Dinner. No entanto, pela minha experiência sei que é melhor manter entidades como Dinner livres de dependências, especialmente se você planeja usar uma ferramenta de mapeamento objeto-relacional como o EF para efetuar o pull delas de uma camada de persistência. Eu não quero ter que popular dependências da entidade como parte desse processo, e o EF certamente não poderá fazer isso sem que haja código adicional de minha parte. Uma abordagem comum neste ponto é tirar a lógica da entidade Dinner e colocá-la em algum tipo de serviço (como DinnerService ou RsvpService) em que dependências podem ser injetadas facilmente. Entretanto, isso tente a levar ao antipadrão de modelo de domínio anêmico (bit.ly/anemic-model) em que as entidades têm pouco ou nenhum comportamento e são apenas montes de estados. Não, neste caso a solução é simples — o método pode simplesmente incorporar a hora atual como um parâmetro e deixar que o código que faz a chamada transmita isso.

Com essa abordagem, a lógica para adicionar uma confirmação de presença é simples (consulte a Figura 2). Este método tem alguns testes que demonstram que ele funciona da forma esperada. Os testes estão disponíveis no projeto de exemplo associado a este artigo.

Figura 2 Lógica de negócios no modelo de domínio

public RsvpResult AddRsvp(string name, string email, DateTime currentDateTime)
{
  if (currentDateTime > RsvpDeadlineDateTime())
  {
    return new RsvpResult("Failed - Past deadline.");
  }
  var rsvp = new Rsvp()
  {
    DateCreated = currentDateTime,
    EmailAddress = email,
    Name = name
  };
  if (MaxAttendees.HasValue)
  {
    if (Rsvps.Count(r => !r.IsWaitlist) >= MaxAttendees.Value)
    {
      rsvp.IsWaitlist = true;
      Rsvps.Add(rsvp);
      return new RsvpResult("Waitlist");
    }
  }
  Rsvps.Add(rsvp);
  return new RsvpResult("Success");
}

Ao deslocar essa lógica para o modelo de domínio, eu garanti que o método da API do meu controlador permanecerá pequeno e focado em suas próprias questões. Como resultado, é fácil testar se o controle faz o que deveria fazer, uma vez que há relativamente poucos caminhos segundo os quais percorrer o método.

Responsabilidades do controlador

Faz parte da responsabilidade do controlador verificar ModelState e garantir que seja válido. Estou fazendo isso no método de ação para fins de clareza, mas em um aplicativo maior eu eliminaria esse código repetitivo de dentro de cada ação usando um Filtro de ação:

[HttpPost]
public IActionResult AddRsvp([FromBody]RsvpRequest rsvpRequest)
{
  if (!ModelState.IsValid)
  {
    return HttpBadRequest(ModelState);
  }

Presumindo que ModelState seja válido, a ação precisa, em seguida, buscar a instância correta de Dinner usando o identificador fornecido na solicitação. Se não encontrar uma instância de Dinner que corresponda à ID, a ação deverá retornar um resultado de Não Encontrado:

var dinner = _dinnerRepository.GetById(rsvpRequest.DinnerId);
if (dinner == null)
{
  return HttpNotFound("Dinner not found.");
}

Após a conclusão dessas verificações, a ação fica livre para delegar a operação de negócios representada pela solicitação ao modelo de domínio, chamando o método AddRsvp na classe Dinner que você viu anteriormente e salvando o estado atualizado do modelo de domínio (neste caso, a instância do jantar e sua coleção de confirmações de presença) antes de retornar uma resposta de OK:

var result = dinner.AddRsvp(rsvpRequest.Name,
    rsvpRequest.Email,
    _systemClock.Now);
  _dinnerRepository.Update(dinner);
  return Ok(result);
}

Lembre-se de que eu decidi que a classe Dinner não deve ter uma dependência do relógio do sistema, optando por fazer com que a hora atual seja transmitida no método. No controlador, estou transmitindo _systemClock.Now para o parâmetro currentDateTime. Este é um campo local que é populado por meio da DI, o que também impede que o controlador seja acoplado de forma rígida ao relógio do sistema. É adequado usar a DI no controlador, em vez de uma entidade de domínio, porque os controladores sempre são criados por contêineres de serviços ASP.NET. Isso atenderá quaisquer dependências que o controlador declarar em seu construtor. _systemClock é um campo do tipo IDateTime, que é definido e implementado em apenas algumas linhas de código:

public interface IDateTime
{
  DateTime Now { get; }
}
public class MachineClockDateTime : IDateTime
{
  public DateTime Now { get { return System.DateTime.Now; } }
}

É claro que eu também preciso garantir que o contêiner ASP.NET seja configurado para usar MachineClockDateTime sempre que uma classe precisar de uma instância de IDateTime. Isso é feito em ConfigureServices na classe Startup e, neste caso, embora qualquer tempo de vida do objeto funcione, eu optei por usar Singleton porque uma instância de MachineClockDateTime funcionará para todo o aplicativo:

services.AddSingleton<IDateTime, MachineClockDateTime>();

Tendo essa simples abstração em vigor, eu posso testar o comportamento do controlador com base no prazo para a confirmação de presença ter ou não sido ultrapassado e garantir que o resultado correto seja retornado. Como já tenho testes para o método Dinner.AddRsvp que confirmam se ele se comporta como esperado, eu não vou precisar de muitos testes do mesmo comportamento pelo controlador para estar convencido de que, ao trabalhar juntos, o controlador e o modelo de domínio estarão trabalhando corretamente.

Próximas Etapas

Baixe o projeto de exemplo associado para ver os testes de unidade para Dinner e DinnersController. Lembre-se de que, normalmente, é muito mais fácil realizar testes de unidade em código com acoplamento livre do que em código com acoplamento rígido cheio de chamadas de método estáticas ou "new" que dependem de aspectos da infraestrutura. “New is glue” e a palavra-chave "new" deve ser usada de maneira ponderada, e não acidentalmente, em seu aplicativo. Saiba mais sobre o ASP.NET Core e seu suporte para injeção de dependência em docs.asp.net.


Steve Smithé um treinador, mentor e consultor independente, bem como um ASP.NET MVP. Ele contribuiu com dezenas de artigos para a documentação oficial do ASP.NET Core (docs.asp.net) e trabalha com equipes que estão aprendendo esta tecnologia. Entre em contato com ele em ardalis.com ou siga-o no Twitter: @ardalis.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Doug Bunting
Doug Bunting é um desenvolvedor que trabalha na equipe de MVC da Microsoft. Ele faz isso há algum tempo e adora o novo paradigma de DI na versão reescrita do MVC Core.