Testes de Integração
Cada projeto gerado a partir dos templates inclui um <YourProject>.Tests.Integration projeto, cabeado e pronto para rodar. Seus testes vão contra um ambiente Dataverse real em vez de mocks, porque a maior parte do que quebra em um portal fica nas costuras que um mock substitui — tradução de consultas, metadados, formatação, permissões. Duas classes base são fornecidas, uma para cada lado disso.
O que tem no Projeto
O template vem esses arquivos; eles são fontes comuns no seu repositório, então alterem-nos livremente.
| Arquivo | Propósito |
|---|---|
PortalEndpointTestBase.cs |
Inicializa seu portal na memória e expõe um HttpClient contra ele. Derive disso para testes de endpoint e permissões. |
TestBase.cs |
Constrói um contêiner de serviço com uma conexão autenticada do Dataverse. Derive disso para chamar seus próprios serviços e lógica diretamente. |
TestAuthentication.cs |
O esquema de autenticação de teste e o TestUserContext que decide para quem uma requisição é executada. |
appsettings.json, xunit.runner.json |
Configurações não secretas, e a configuração do xunit runner que mantém as coleções seriadas. |
ExampleTests.cs, README.md |
Exemplos funcionais de ambas as bases (pulando até apontar para registros reais), e uma referência local curta. |
Escolhendo uma Base
Os dois são complementares — testam camadas diferentes, e um conjunto saudável usa ambos.
PortalEndpointTestBaseExecuta seu , entãoPrograma requisição passa pela ordem real do middleware, pela autorização real e pelos manipuladores de permissões exatamente como você os registrou. Use para permissões de tabela, a superfície do/api/*framework e qualquer endpoint que você adicionar. Como é sua aplicação, um teste aqui não pode se desviar da fiação de produção.TestBaseInicia sem host. Ele constrói um container e te entrega um cliente Dataverse, o que o torna a escolha mais barata para lógica no estilo plugin, serviços de domínio e formas de consulta. Não vê HTTP, então não se aplica middleware, autorização ou manipuladores de permissões.
Configuração e Segredos
Aponte D365:Url para o ambiente contra o qual os testes devem rodar em appsettings.json — um ambiente de desenvolvimento ou descartável, já que os testes criam e excluem registros. Credenciais vêm de segredos de usuário, e o projeto de teste declara deliberadamente o mesmo UserSecretsId que o projeto web, então um portal que já roda localmente não precisa de mais nada. Para defini-las explicitamente:
dotnet user-secrets set "D365:ClientId" "<application (client) id>"
dotnet user-secrets set "D365:ClientSecret" "<client secret value>"
Fontes são sobrepostas appsettings.json → segredos de usuário → variáveis de ambiente, então um agente de build pode fornecer tudo através de variáveis de ambiente (D365__ClientSecretusando o duplo sublinhado) sem tocar em um arquivo. Nunca coloque o segredo do cliente em appsettings.json, que é commitado.
Nota
Você não precisa de uma chave de licença para rodar testes. O host em memória ouve em um endereço de loopback, e o loopback é licenciado sem um — sem registro do site do portal e sem necessidade de dupla de teste.
Testando Endpoints e Permissões
Derive de PortalEndpointTestBase e faça requisições com Client. As requisições começam anônimas, que é o padrão correto para afirmar que um endpoint afasta chamadores anônimos. Redirecionamentos não são seguidos, então um teste pode afirmar em um 302 em vez da página em que ele cairia.
public class AccountPermissionTests : PortalEndpointTestBase
{
[Fact]
public async Task AnonymousCallersAreRefused()
{
// No sign-in: the request is anonymous.
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task SignedInContactCanRead()
{
this.SignInAs(contactId);
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
Atuando como Usuário
SignInAs é a que deve ser alcançada. Ele faz stubs apenas na etapa de credencial — o principal que emite carrega as mesmas reivindicações que um login real emite, então a autorização, os manipuladores de permissões e toda extensão de leitura de reivindicações se comportam como em produção. Sem senha, sem provisionamento, e pode mudar de identidade entre requisições em um único teste.
LoginAsync Posta credenciais reais no endpoint de login e mantém o cookie, para quando o próprio caminho de login estiver sendo testado. Ele precisa de um contato com senha e um e-mail confirmado, então é mais lento; os dois coexistem e a base volta ao cookie real sempre que nenhum usuário imitado é definido.
// Impersonate — fast, no password, still runs the real authorization path.
this.SignInAs(contactId);
this.SignInAs(contactId, "Administrator", "Manager"); // with role claims
this.SignInAs(userId, PortalUserType.SystemUser); // an internal user
this.SignOut(); // anonymous again
// Or sign in for real, when the sign-in path itself is what's under test.
var login = await this.LoginAsync("someone@example.com", "password");
Nota
LoginAsyncprecisa dos endpoints de autenticação JSON, que são mapeados para os hosts React e Blazor WebAssembly/Auto. Um projeto Blazor renderizado por servidor faz login por meio de posts de formulário nas páginas da conta Razor, então/api/auth/loginnão é mapeado ali eLoginAsyncretorna 404 — useSignInAs, que funciona em todos os hosts.
Preparação, Desmontagem e Substituições
Três ganchos cobrem as necessidades habituais. ConfigureTestServices Executa após os próprios registros da sua aplicação, então um Replace lá vence Program.cs — a forma de posicionar um falso para uma dependência de saída. OnInitializedAsync e OnDisposingAsync coloque cada teste entre parchetes para seed e limpeza, e a desmontagem roda mesmo quando um teste falha.
public class OrderTests : PortalEndpointTestBase
{
private Guid _contactId;
// Replace a service in the running host — a fake for an outbound dependency, say.
protected override void ConfigureTestServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IShippingQuotes, FakeShippingQuotes>());
}
// Seed what the test needs. Runs after the host starts.
protected override async Task OnInitializedAsync()
{
_contactId = await this.CreateTestContactAsync();
}
// Always runs, including after a failing test.
protected override async Task OnDisposingAsync()
{
await this.DeleteContactAsync(_contactId);
}
}
Importante
Esses testes iniciam seu portal, então qualquer coisa que o impeda de começar também os para. Em particular, com a autorização aprimorada ativada, o host valida o ID do site configurado na inicialização e falha com orientação até que um id real
powerpagesiteseja fornecido — a mesma pré-condição de rodar o portal.
Serviços de Teste e Lógica
Derive de TestBase, registrando o que o código sob teste precisa e resolvendo. O cliente Dataverse já está autenticado.
public class InvoiceLogicTests : TestBase
{
// Register what the code under test needs; always call base first.
protected override void RegisterServices(IServiceCollection services)
{
base.RegisterServices(services);
services.AddMyPortalLogic();
}
[Fact]
public async Task PostingAnInvoiceSetsTheBalance()
{
var logic = this.ServiceProvider.GetRequiredService<InvoiceLogic>();
// A Dataverse client, already authenticated. IOrganizationService resolves to the
// same connection, so resolving either doesn't open a second one.
using var service = this.CreateOrganizationService();
var invoice = await service.Queryable("invoice")
.FirstOrDefaultAsync(i => i.GetAttributeValue<string>("invoicenumber") == "INV-1");
invoice.Should().NotBeNull();
}
}
Passe um ID de usuário para CreateOrganizationService(userId) se passar — O Dataverse então aplica os próprios privilégios desse usuário, que é como verificar o acesso em nível de registro pelo lado dos dados em vez de por um endpoint.
CreateWebApiHttpClientAsync() retorna um cliente autenticado contra a API Web do Dataverse (/api/data/v9.2/) para asserções que são mais fáceis de expressar em OData do que através do SDK. Ele precisa Azure:TenantId de configuração, além do id do cliente e do segredo.
Como os Testes Funcionam
Execute-as com dotnet test, ou pelo explorador de testes do seu IDE. Algumas características que valem a pena conhecer antes de escrever muitas:
- Coleções rodam em série. Elas compartilham um único ambiente Dataverse, e gravações paralelas nos mesmos registros dificultam a reprodução de falhas. Isso é definido em
xunit.runner.json. - O xUnit constrói uma nova instância da classe de teste para cada método de teste. A conexão autenticada do Dataverse é armazenada em cache para todo o processo e distribuída como um clone leve, então isso custa um login por execução em vez de um por teste — mas qualquer coisa cara que você adicione a um construtor paga em cada teste.
- Limpe o que você criar. Um teste que deixa registros para trás faz com que as asserções da próxima execução dependam da última. Prefira valores únicos por teste (um GUID em um campo de nome) em vez de valores fixos, para que execuções repetidas não colidam.
Melhores práticas
Aponte o conjunto para um ambiente de desenvolvimento, nunca para produção. Essas são gravações reais contra dados reais, e um teste que exclui o que foi criado apagará o registro real se você mirar em um.
