Comecei a escrever alguns testes de unidade para o meu projeto atual. Eu realmente não tenho experiência com isso, no entanto. Primeiro, quero "obtê-lo" completamente, portanto, atualmente não estou usando nem minha estrutura de IoC nem uma biblioteca de simulação.
Fiquei me perguntando se há algo errado em fornecer argumentos nulos para construtores de objetos em testes de unidade. Deixe-me fornecer um código de exemplo:
public class CarRadio
{...}
public class Motor
{
public void SetSpeed(float speed){...}
}
public class Car
{
public Car(CarRadio carRadio, Motor motor){...}
}
public class SpeedLimit
{
public bool IsViolatedBy(Car car){...}
}
Outro exemplo de código de carro (TM), reduzido a apenas as partes importantes para a questão. Agora eu escrevi um teste mais ou menos assim:
public class SpeedLimitTest
{
public void TestSpeedLimit()
{
Motor motor = new Motor();
motor.SetSpeed(10f);
Car car = new Car(null, motor);
SpeedLimit speedLimit = new SpeedLimit();
Assert.IsTrue(speedLimit.IsViolatedBy(car));
}
}
O teste corre bem. SpeedLimit
precisa de um Car
com um Motor
para fazer sua coisa. Ele não está interessado em CarRadio
nada, por isso forneci nulo para isso.
Gostaria de saber se um objeto que fornece a funcionalidade correta sem ser totalmente construído é uma violação do SRP ou um cheiro de código. Tenho essa sensação incômoda, mas speedLimit.IsViolatedBy(motor)
também não parece certa - um limite de velocidade é violado por um carro, não por um motor. Talvez eu só precise de uma perspectiva diferente para testes de unidade e código de trabalho, porque toda a intenção é testar apenas uma parte do todo.
A construção de objetos com null em testes de unidade é um cheiro de código?
fonte
Motor
provavelmente não deveria ter umspeed
. Ele deve ter umthrottle
e calcular um comtorque
base no atualrpm
ethrottle
. O trabalho do carro é usar umTransmission
para integrar isso à velocidade atual e transformá-lo em umrpm
sinal de volta para oMotor
... Mas eu acho que você não estava interessado no realismo, estava?null
rádio, o limite de velocidade é calculado corretamente. Agora você pode criar um teste para validar o limite de velocidade com um rádio; apenas no caso de o comportamento variar ...Respostas:
No caso do exemplo acima, é razoável que a
Car
possa existir sem aCarRadio
. Nesse caso, eu diria que não apenas é aceitável passar um nuloCarRadio
às vezes, como também é obrigatório. Seus testes precisam garantir que a implementação daCar
classe seja robusta e não lança exceções de ponteiro nulo quando nãoCarRadio
houver nenhuma .No entanto, vamos dar um exemplo diferente - vamos considerar a
SteeringWheel
. Vamos supor que aCar
tenha que ter umaSteeringWheel
, mas o teste de velocidade não se importa com isso. Nesse caso, eu não passaria um nulo,SteeringWheel
pois isso levaria aCar
classe a lugares onde ela não foi projetada para ir. Nesse caso, seria melhor criar algum tipo deDefaultSteeringWheel
(para continuar a metáfora) bloqueado em uma linha reta.fonte
Car
não pode ser construído sem umaSteeringWheel
...Car
sem umCarRadio
, não faria sentido criar um construtor que não exija que um seja passado e que não precise se preocupar com um nulo explícito ?Car
lançariam um NRE com um nuloCarRadio
, mas o código relevante paraSpeedLimit
não. No caso da pergunta postada agora, você estaria correto e eu realmente faria isso.Sim. Observe a definição C2 de cheiro de código : "um CodeSmell é uma dica de que algo pode estar errado, não uma certeza".
Se você está tentando demonstrar que
speedLimit.IsViolatedBy(car)
não depende doCarRadio
argumento transmitido ao construtor, provavelmente seria mais claro para o futuro mantenedor tornar essa restrição explícita, introduzindo um teste duplo que falha no teste se for invocado.Se, como é mais provável, você estiver apenas tentando salvar o trabalho de criar um
CarRadio
que você sabe que não é usado nesse caso, observe queCarRadio
não é usado vazar para o teste)Car
sem especificar umCarRadio
Eu suspeito fortemente que o código esteja tentando dizer que você deseja um construtor que se pareça com
e então esse construtor pode decidir o que fazer com o fato de que não há
CarRadio
ou
ou
etc.
Esse é um segundo cheiro interessante - para testar o SpeedLimit, você precisa construir um carro inteiro em torno de um rádio, mesmo que a única coisa que a verificação do SpeedLimit provavelmente se preocupa seja com a velocidade .
Essa pode ser uma dica que
SpeedLimit.isViolatedBy
deve estar aceitando uma interface de função que o carro implementa , em vez de exigir um carro inteiro.IHaveSpeed
é um nome realmente ruim; espero que seu idioma de domínio melhore.Com essa interface, seu teste
SpeedLimit
pode ser muito mais simplesfonte
IHaveSpeed
interface, pois (pelo menos em c #) você não seria capaz de instanciar uma interface em si.Não
De modo nenhum. Pelo contrário, se
NULL
houver um valor disponível como argumento (alguns idiomas podem ter construções que não permitem a passagem de um ponteiro nulo), você deve testá-lo.Outra pergunta é o que acontece quando você passa
NULL
. Mas é exatamente disso que trata um teste de unidade: garantir que um resultado definido esteja acontecendo quando para cada entrada possível. ENULL
é uma entrada potencial, portanto, você deve ter um resultado definido e um teste para isso.Portanto, se seu carro deve ser construído sem rádio, você deve escrever um teste para isso. Se não é e deve lançar uma exceção, você deve escrever um teste para isso.
De qualquer forma, sim, você deve escrever um teste
NULL
. Seria um cheiro de código se você o deixasse de fora. Isso significa que você tem um ramo de código não testado, que você supôs que magicamente estaria livre de erros.Observe que concordo com as outras respostas de que seu código em teste pode ser aprimorado. No entanto, se o código aprimorado tiver parâmetros que são ponteiros, passar
NULL
mesmo para o código aprimorado é um bom teste. Eu gostaria de um teste para mostrar o que acontece quando eu passarNULL
como motor. Não é um cheiro de código, é o oposto de um cheiro de código. Está testando.fonte
Car
aqui. Embora não veja nada que consideraria uma afirmação errada, a questão é sobre o testeSpeedLimit
.NULL
para o construtor deCar
.SpeedLimit
. Você pode ver que o teste é chamado "SpeedLimitTest" e o únicoAssert
está verificando um método deSpeedLimit
. TestesCar
- por exemplo "se seu carro for construtível sem rádio, você deve escrever um teste para isso" - aconteceria em um teste diferente.Eu, pessoalmente, hesitaria em injetar nulos. De fato, sempre que injeto dependências em um construtor, uma das primeiras coisas que faço é gerar uma ArgumentNullException se essas injeções forem nulas. Dessa forma, eu posso usá-los com segurança dentro da minha classe, sem dezenas de cheques nulos espalhados. Aqui está um padrão muito comum. Observe o uso de readonly para garantir que as dependências definidas no construtor não possam ser definidas como nulas (ou qualquer outra coisa) posteriormente:
Com o exposto, talvez eu possa fazer um teste de unidade ou dois, onde injeto nulos, apenas para garantir que minhas verificações nulas funcionem conforme o esperado.
Dito isto, isso se torna um problema, uma vez que você faz duas coisas:
Então, para o seu exemplo, você teria:
Mais detalhes sobre o uso do Moq podem ser encontrados aqui .
Obviamente, se você quiser entendê-lo sem zombar primeiro, ainda poderá fazê-lo, desde que esteja usando interfaces. Simplesmente crie um CarRadio e Motor fictício da seguinte forma e injete-os:
fonte
IMotor
tivesse umgetSpeed()
método, tambémDummyMotor
seria necessário retornar uma velocidade modificada após um êxitosetSpeed
.Não está tudo bem, é uma técnica bem conhecida de quebra de dependência para colocar o código legado em um equipamento de teste. (Consulte “Trabalhando efetivamente com o código legado”, de Michael Feathers.)
Cheira? Bem...
O teste não cheira. O teste está fazendo seu trabalho. Ele está declarando "este objeto pode ser nulo e o código em teste ainda funciona corretamente".
O cheiro está na implementação. Ao declarar um argumento construtor, você está declarando "essa classe precisa dessa coisa para funcionar corretamente". Obviamente, isso é falso. Qualquer coisa falsa é um pouco fedorenta.
fonte
Vamos voltar aos primeiros princípios.
No entanto, haverá construtores ou métodos que tomam outras classes como parâmetros. A maneira como você lida com isso varia de caso para caso, mas a configuração deve ser mínima, pois é tangencial ao objetivo. Pode haver situações em que a passagem de um parâmetro NULL seja perfeitamente válida - como um carro sem rádio.
Estou assumindo aqui que estamos apenas testando a linha de corrida (para continuar com o tema do carro), mas você também pode passar NULL para outros parâmetros para verificar se algum código defensivo e mecanismos de tratamento de exceções estão funcionando como requeridos.
Por enquanto, tudo bem. No entanto, e se NULL não for um valor válido. Bem, aqui você está no mundo sombrio de zombarias, tocos, manequins e falsificações .
Se tudo isso parecer um pouco demais, você já está usando um manequim passando NULL no construtor Car, pois é um valor que potencialmente não será usado.
O que deve ficar claro rapidamente, é que você está potencialmente executando o código com muita manipulação NULL. Se você substituir os parâmetros de classe por interfaces, também abrirá o caminho para zombaria e DI etc, o que os torna muito mais simples de lidar.
fonte
Dando uma olhada no seu design, eu sugeriria que a
Car
é composto por um conjunto deFeatures
. Um recurso pode ser umCarRadio
, ouEntertainmentSystem
que pode consistir em coisas como umCarRadio
,DvdPlayer
etc.Então, no mínimo, você teria que construir um
Car
com um conjunto deFeatures
.EntertainmentSystem
eCarRadio
seriam implementadores da interface (Java, C # etc) dopublic [I|i]nterface Feature
e isso seria uma instância do padrão de design Composite.Portanto, você sempre construiria um
Car
comFeatures
e um teste de unidade nunca instanciaria umCar
sem nenhumFeatures
. Embora você possa considerar zombar de umBasicTestCarFeatureSet
que tenha um conjunto mínimo de funcionalidades necessárias para o teste mais básico.Apenas alguns pensamentos.
John
fonte