Como testes ajudam a melhorar o design do código?
25243 단어 elixirbraziliandevstesting
부작용
Um dos principais problemas que eu enfrentei escrevendo os testes foi de que o código que eu precisava testar dependsia de arquivos do sistema:
with {:ok, content} <- File.read(path),
results <- Core.log_to_results(content, opts[:mode]) do
{:ok, results}
end
Lidar com esse tipo de efeito colateral uma única vez não me causou nenhum problema, principalmente quando se tratava apenas de leitura de arquivos.
Porém, ao implementar um processo que notificava as mudanças nos arquivos de logs, acabei escrevendo testes que criavam arquivos temporários, como na imagem abaixo:
Então enfrentei dois problemas com essa abordagem:
Há muito tempo eu li um post do José Valim sobre Mocks and explicit contracts , e isso me inspirou a refatorar o projeto para não dependser mais de
File.read/1
e File.stats/1
. 로고 크리에이 오 동작Log
:defmodule Q3Reporter.Log do
@moduledoc false
alias Q3Reporter.Log.FileAdapter
@type read_return :: {:ok, String.t()} | {:error, atom()}
@type mtime_return :: {:ok, NaiveDateTime.t()} | {:error, atom()}
@callback read(String.t()) :: read_return
@callback mtime(String.t()) :: mtime_return
@default_adapter Application.compile_env(:q3_reporter, :log_adapter, FileAdapter)
@spec read(String.t(), atom() | nil) :: read_return()
def(read(path, adapter \\ @default_adapter))
def read(path, adapter) when is_nil(adapter), do: @default_adapter.read(path)
def read(path, adapter), do: adapter.read(path)
@spec mtime(String.t(), atom() | nil) :: mtime_return()
def mtime(path, adapter \\ @default_adapter)
def mtime(path, adapter) when is_nil(adapter), do: @default_adapter.mtime(path)
def mtime(path, adapter), do: adapter.mtime(path)
end
A vantagem dessa abordagem é de que eu não precisava mais saber como a leitura dos logs era feita. Era necessário apenas respeitar o contrato e receber
{:ok, conteúdo}
para o read/1
. E também não precisioni mais tratar o resultado do File.stats/1
, já que o contrato de mtime/1
, que era {:ok, novo_mtime}
, já trazia o dado que eu precisava. Logo, a implementação do Log.FileAdapter
ficou:defmodule Q3Reporter.Log.FileAdapter do
@behaviour Q3Reporter.Log
@impl true
def read(path), do: File.read(path)
@impl true
def mtime(path) do
case File.stat(path) do
{:ok, stat} -> NaiveDateTime.from_erl(stat.mtime)
{:error, _} = error -> error
end
end
end
Então eu pude criar uma implementação que simplesmente manipulava os "arquivos"na memória utilizando ETS:
defmodule Q3Reporter.Log.ETSAdapter do
@behaviour Q3Reporter.Log
@table __MODULE__
@impl true
def read(name) do
case :ets.lookup(@table, name) do
[{_name, content, _mtime}] -> {:ok, content}
_ -> {:error, :enoent}
end
end
@impl true
def mtime(name) do
case :ets.lookup(@table, name) do
[{_name, _content, mtime}] -> {:ok, mtime}
_ -> {:error, :enoent}
end
end
@doc false
def init, do: :ets.new(@table, [:named_table, :set, :public])
@doc false
def close(name), do: :ets.delete(@table, name)
@doc false
def push(name, content \\ "", mtime \\ NaiveDateTime.utc_now()) do
:ets.insert(@table, {name, content, mtime})
end
end
E configurar meus testes para utilizar esse adapter por padrão em
tests.exs
:config :q3_reporter, log_adapter: Q3Reporter.Log.ETSAdapter
로고는 시스템에 의존하지 않고 조작할 수 없습니다. E caso precisasse realizar testes que liam arquivos reais, eu apenas tinha de passar o
Log.FileAdapter
para a função Q3Reporter.parse/2
:test "should return game contents with valid file and default mode" do
assert {:ok, results} = Q3Reporter.parse(@path, mode: :by_game, log_adapter: FileAdapter)
assert %Results{entries: [%{}], mode: :by_game} = results
end
코베르투라 데 고환
Enfrentei outro problema ao tentar testar erros relacionados a
File.read/1
, como "falta de memória"및 "permissões". Porém não é viável eu manipular arquivos os quais eu não tenho permissões no projeto, e nem lotar a memória do meu computador enquanto os testes eram realizados.A minha primeira solução, no caso, foi ignorar esses trechos de código na cobertura de testes:
def parse(path, opts \\ []) do
with {:ok, content} <- Log.read(path, opts[:log_adapter]),
results <- Core.log_to_results(content, opts[:mode]) do
{:ok, results}
else
{:error, :enoent} ->
{:error, "'#{path}' not found..."}
# coveralls-ignore-start
{:error, :eacces} ->
{:error, "You don't have permission to open '#{path}..."}
{:error, :enomem} ->
{:error, "There's no enough memory to open 'invalid'..."}
{:error, _} ->
{:error, "Error trying to open 'invalid'"}
# coveralls-ignore-stop
end
end
Como a refatoração da sessão anterior me permitiu substituir o adapter, logo eu poderia testar esse trecho da seguinte maneira:
test "should return error with invalid file path" do
assert {:error, "'invalid' not found..."} =
Q3Reporter.parse("invalid", log_adapter: FileAdapter)
assert {:error, "You don't have permission to open 'invalid'..."} =
Q3Reporter.parse("invalid", log_adapter: error_adapter(:eacces))
assert {:error, "There's no enough memory to open 'invalid'..."} =
Q3Reporter.parse("invalid", log_adapter: error_adapter(:enomem))
assert {:error, "Error trying to open 'invalid'"} =
Q3Reporter.parse("invalid", log_adapter: error_adapter(:unknown))
end
O
error_adapter/1
não é nada mais do que uma função que utiliza o Mox para mokar um adapter, e retornar o erro esperado:def error_adapter(error) do
module = module_name(error)
Mox.defmock(module, for: Q3Reporter.Log)
module
|> expect(:read, fn _ -> {:error, error} end)
|> expect(:mtime, fn _ -> {:error, error} end)
end
Logo, substituir o
File
por um contrato explícito através de Log
me permitiu atingir uma cobertura de testes onde não era possível anteriormente, e eu pude tirar todos os #coveralls-ignore
do projeto e ter 100% de cobertura de testes real.É possível conferir o código de exemplo desse projeto em: https://github.com/maxmaccari/q3_reporter
Reference
이 문제에 관하여(Como testes ajudam a melhorar o design do código?), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/maxmaccari/como-tests-ajudam-a-melhorar-o-design-do-codigo-57c9텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)