rOpenSci | Um exemplo dos princípios DRY/DAMP para testes de pacotes

Um exemplo dos princípios DRY/DAMP para testes de pacotes

rOpenSci’s O segundo grupo de campeões e campeãs foi integrado! O treinamento começou com uma sessão sobre estilo de código, foi seguido por três sessões sobre os fundamentos do desenvolvimento de pacotes R e terminou com uma sessão sobre desenvolvimento avançado de pacotes R que consistiu em um pot-pourri de dicas com discussão, seguido de tempo para aplicar esses princípios aos pacotes das pessoas participantes. Aqui, quero compartilhar um dos tópicos abordados: Testes de pacotes e, em particular, os princípios DRY (“não se repita”) e DAMP (“frases descritivas e significativas”). Para esse tópico, usamos um repositório do GitHub que contém um pacote R cujos diferentes commits ilustram os dois princípios. Em cada etapa, compartilharemos um commit ou diff que ilustra as alterações feitas.

🔗 Etapa 1: lamacento!

Primeiro commit: Configurar nossos arquivos de teste, test-works.R e test-ok.R

Esses arquivos de teste iniciais definem um objeto chamado test_object no nível superior, que é usado em dois testes em cada arquivo.

test-works.R

test_object <- list(a = 1, b = 2)

test_that("multiplication works", {
  expect_equal(test_object[["b"]] * 2, 4)
})

test_that("addition works", {
  expect_equal(test_object[["a"]] + 2, 3)
})

e

test-ok.R

test_object <- list(a = 1, b = 2)

test_that("division works", {
  expect_equal(test_object[["b"]] / 2, 1)
})

test_that("substraction works", {
  expect_equal(test_object[["a"]] - 1, 0)
})

Esse não é um padrão ideal porque você não pode examinar cada teste test_that() isoladamente e entender rapidamente o que está acontecendo.

Em um arquivo de teste muito longo, você teria que rolar a tela para cima e para baixo! Além disso, estamos sendo repetitivos ao definir o mesmo objeto de teste em dois arquivos de teste.

🔗 Estágio 2: SECO!

Próximo commit: Descompactar esses arquivos

Nesta etapa, lembramos diligentemente sobre DRY, Don’t Repeat Yourself (Não se repita), e sobre a mecânica de arquivos auxiliares de teste: arquivos cujos nomes começam com helper- são carregados antes de todos os testes.

Portanto, criamos um arquivo auxiliar (helper-swamp.R) no qual o test_object é definido e, desta forma, disponível para testes!

Em tests/testthat/helper-swamp.R,

test_object <- list(a = 1, b = 2)

Nos arquivos de teste, removemos a primeira linha que definia test_object.

Agora as coisas ainda não estão perfeitas. Quando olhamos para qualquer um dos arquivos de teste, não podemos realmente saber o que é test_object, pois seu nome não é “descritivo e significativo”.

Além disso, agora temos test_object que é sempre definido, mesmo que não seja usado em um teste. Na melhor das hipóteses, isso é desnecessário e inútil; na pior, pode ter efeitos colaterais indesejados, especialmente em códigos mais complexos! 1

🔗 Etapa 3: Foco no DAMP

Terceiro compromisso: Aplicar os princípios do DAMP

No arquivo auxiliar, tests/testthat/helper-swamp.R reescrevemos o código em um arquivo função com um nome mais significativo (pelo menos vamos fingir que é!).

basic_list <- function() {
  list(a = 1, b = 2)
}

Em seguida, chamamos essa função para definir o objeto em todos os testes em que ele for necessário. Assim, os arquivos de teste se tornam

test_that("division works", {
  test_object <- basic_list()
  expect_equal(test_object[["b"]] / 2, 1)
})

e

test_that("substraction works", {
  test_object <- basic_list()
  expect_equal(test_object[["a"]] - 1, 0)
})

Agora, embora a definição real da lista básica não esteja em todos os testes, temos uma ideia melhor do que está acontecendo ao ler o teste.

Além disso, se o teste falhasse, poderíamos executar devtools::load_all() no console e executar o código do teste, já que devtools::load_all() carrega os arquivos auxiliares do testthat, tornando basic_list() disponível.

🔗 Conclusão

O equilíbrio entre DRY (“Don’t repeat yourself”) e DAMP (“Descriptive and meaningful phrases”) é uma troca. Para manter a analogia com a água, também precisamos garantir que nosso código não tenha efeitos que possam “vazar” inesperadamente. O que devemos buscar são testes autônomos que possamos entender e executar sem muito contexto.

Outra consideração que não abordamos aqui são os testes que exigem elementos específicos, como variáveis de ambiente ou opções. Nesses casos, tente usar withr como withr::local_envvar() em cada teste que o exigir.

Uma ideia poderosa do livro “Software Engineering at Google” de Titus Winters, Tom Manshreck e Hyrum Wright, é que o código pode se dar ao luxo de ser um pouco menos óbvio porque tem testes que o cobrem, mas o código de teste, que não é coberto por testes, não tem esse privilégio.

🔗 Outros recursos


  1. Isso é um vazamento. Em outro teste, você poderia se perguntar por que um objeto existe, por que uma opção específica foi definida, etc., e isso é um pesadelo para depurar. ↩︎