Sitemap

Angular: Testando Deferrable Views

5 min readAug 24, 2024

--

Nesse post vamos entender como testar um dos mais recentes recursos introduzidos no Angular, as Deferrable Views.

Press enter or click to view image in full size

Contextualizando

Desde a versão 17 o Angular vem passando por um processo de modernização e renascimento, caracterizado pela introdução de uma série de recursos que buscam melhorar a experiência de desenvolvimento, diminuir a curva de aprendizado e tornar o framework mais performático e completo.

Um desses novos recursos foram as Deferrable Views, que nos permite fazer o lazy loading dos elementos da nossa aplicação a nível de template, por exemplo:

@defer {
<app-list [posts]="posts()"/>
} @loading {
<p>Carregando posts...</p>
} @error {
<p>Erro ao tentar carregar posts...</p>
}

Usamos a declaração @defer no nosso template para indicar que tudo que está no bloco entre a sua chave de abertura e fechamento deve ser carregado de forma tardia, nesse exemplo estamos adiando o carregamento de um componente que renderiza uma lista até que os dados estejam disponíveis para ser renderizado.

Podemos ainda declarar um bloco @loading para renderizar um estado de carregamento e um bloco @error para dar um feedback ao usuário caso houver problema ao carregar o componente de lista.

Testando Deferrable Views

Bem, toda essa inclusão de recursos levanta a discussão sobre testes, e com as Deferrable Views não é diferente, e é disso que vamos aprofundar agora.

O Angular gerencia a renderização dos blocos no contexto das Deferrable Views, ele decide quando cada bloco será renderizado para o usuário, no entanto, num contexto de teste onde queremos reproduzir e validar os comportamentos da nossa View o ideal é que tenhamos mais controle sobre isso.

Felizmente o Angular disponibiliza uma API que nos permite disparar os diferentes estados respectivos a cada um desses blocos, por exemplo:


// Renderiza o estado final
await deferBlockFixture.render(DeferBlockState.Complete);

// Renderiza o estado de loading respectivo ao bloco @loading
await deferBlockFixture.render(DeferBlockState.Loading);

// Renderiza o estado de erro respectivo ao bloco @error
await deferBlockFixture.render(DeferBlockState.Error);

// Não incluímos no nosso exemplo, mas
// caso tivesse poderiamos renderizar o estado respectivo
// ao bloco @placeholder
await deferBlockFixture.render(DeferBlockState.Placeholder);

Basicamente, podemos controlar a renderização manualmente e consequentemente controlar o comportamento da nossa View, vamos ver um exemplo completo agora…

Preparando o ambiente

Vamos implementar alguns testes no nosso componente anterior, que serão o seguinte:

  • Testar o cenário feliz, onde a lista de posts foi renderizada;
  • Testar o cenário infeliz, em que houve um erro ao renderizar a lista;
  • Testar o cenário de carregamento, onde o componente de lista ainda está sendo preparado para renderizar;
  • E um plus, testando cenário onde a lista carrega com sucesso, porém está vazia.

Porém, antes de começar, precisamos configurar o ambiente, vamos usar o seguinte biblioteca Angular Testing Library e o Jest, para configurar no seu ambiente use os links abaixo:

Escrevendo os testes

Bora pro primeiro teste, nosso componente responsável por renderizar a lista usa um serviço que requisita dados de uma API, vamos criar um mock para o nosso serviço:

Get Danilo Lima’s stories in your inbox

Join Medium for free to get updates from this writer.

Criando nosso mock

const mockedPosts: Post[] = [
{
"id": 1,
"title": "First Post",
"body": "This is the first post",
"tags": [],
"reactions": {
"likes": 192,
"dislikes": 25
},
"views": 305,
"userId": 121
},
{
"id": 2,
"title": "Second Post",
"body": "This is the second post",
"tags": [],
"reactions": {
"likes": 192,
"dislikes": 25
},
"views": 305,
"userId": 121
},
]

Configurando o teste

describe('AppComponent', () => {
const defaultConfig = {
// Perceba que precisamos mudar o comportamento dos blocos defer
// para manual
deferBlockBehavior: DeferBlockBehavior.Manual,
providers: [
{
provide: PostService,
useValue: {
getPosts() {
return of({
limit: 10,
posts: mockedPosts,
skip: 0,
total: mockedPosts.length
} as GetPostsResponse)
}
}
}
],
}
// ...

Testando o cenário feliz — lista renderizada

  it('should display post list', async () => {

const { fixture } = await render(AppComponent, {
...defaultConfig
});

// Obtemos referência ao nosso bloco @defer no template
const deferBlockFixture = (await fixture.getDeferBlocks())[0];

// Renderizamos o estado final
await deferBlockFixture.render(DeferBlockState.Complete);

// Verificamos se os dois itens da lista estão renderizados
expect(screen.getByText(mockedPosts[0].title)).toBeInTheDocument();
expect(screen.getByText(mockedPosts[1].title)).toBeInTheDocument();
});

Testando cenário de carregamento

  it('show display loading message while posts are still being fetched', async () => {

const { fixture } = await render(AppComponent, {
...defaultConfig
});

const deferBlockFixture = (await fixture.getDeferBlocks())[0];

await deferBlockFixture.render(DeferBlockState.Loading);

expect(screen.getByText(/Carregando posts.../i)).toBeInTheDocument();
});

Testando o cenário infeliz — erro ao carregar posts

it('show display error message when fetching posts returns an error', async () => {

// Sobrescrevemos o provider para simular
// um erro vindo do PostService usando a função throwError do RxJS
const { fixture } = await render(AppComponent, {
...defaultConfig,
providers: [
{
provide: PostService,
useValue: {
getPosts(){
return throwError(() => new Error('Error on loading posts'))
}
}
}
]
});

const deferBlockFixture = (await fixture.getDeferBlocks())[0];

await deferBlockFixture.render(DeferBlockState.Error);

const errorMsgRegex = /Erro ao tentar carregar posts.../i;

expect(screen.getByText(errorMsgRegex)).toBeInTheDocument();
});

Testando cenário de lista vazia

O nosso componente de lista utiliza o novo Control Flow do Angular, que foi um dos recursos introduzidos nas novas versões.

Aqui usamos a declaração @for para iterar sobre a lista de posts e renderizar cada um dos itens, o interessante é que podemos definir um bloco @empty, que renderiza um conteúdo quando a lista estiver vazia.

<ul>
@for (item of posts(); track item) {
<li data-test="post">
<h2>{{item.title}}</h2>
<p>{{item.body}}</p>
<div>
<span>Views: {{item.views}}</span>
<span>Likes {{item.reactions.likes}}</span>
</div>
</li>
} @empty {
<p>Nenhum post para ser exibido</p>
}
</ul>

E para testar, só precisamos renderizar o estado final do bloco @defer:

  it('show display empty state message when fetching posts returns an empty list', async () => {

const { fixture } = await render(AppComponent, {
...defaultConfig,
// Sobrescrevemos o PostService para retornar uma lista vazia
providers: [
{
provide: PostService,
useValue: {
getPosts(){
return of([])
}
}
}
]
});

const deferBlockFixture = (await fixture.getDeferBlocks())[0];

// Renderizando o estado completo do bloco
await deferBlockFixture.render(DeferBlockState.Complete);

const emptyStateMsg = /Nenhum post para ser exibido/i;

expect(screen.getByText(emptyStateMsg)).toBeInTheDocument();
});

Resultado dos nossos testes:

Conclusão

As Deferrable Views são um dos melhores recursos incluídos no Angular nos últimos tempos, com ela podemos adiar o carregamento de componentes, diretivas, pipes e qualquer CSS associado a eles.

E adiar o carregamento também significa que o Angular cria um bundle separado para esses elementos. A API é muito poderosa e recomendo dar uma olhada com mais profundidade, podemos ir muito além, só para mencionar, podemos adiar o carregamento de uma View até a iteração do usuário com a UI, por exemplo.

Os testes nesse recurso também traz desafios, mas com uma API simples a experiência de desenvolvimento dos testes se torna mais suave!

E ai, o que achou ?

Se quiser apoiar o meu trabalho reaja a esse post se te ajudei de alguma forma, compartilhe e me siga por aqui ou nas outras redes pra ficar por dentro dos próximos conteúdos.

Até logo, dev! 😉

--

--

Danilo Lima
Danilo Lima

Written by Danilo Lima

Software developer working mainly with Frontend and Angular, Open Source Contributor and I usually give my 20 cents about technology and career.