Usando stubs para isolar partes de seu aplicativo para testes de unidade
Os tipos de stub são uma das duas tecnologias que a estrutura de falsificação Microsoft fornece para o deixar facilmente isolar um componente que você está testando de outros componentes que chama.Um stub é uma pequena parte do código que usa o local de outro componente durante testes.A vantagem de usar um stub é que retorna os resultados consistentes, facilitando o teste gravação.E você pode executar testes mesmo se os outros componentes não estão trabalhando ainda.
Para uma guia da visão geral e de início rápido a falsificação, consulte Isolando código em teste com falsificação da Microsoft.
Para usar modelos, você precisa escrever seu componente que ele use apenas classes, interfaces, não para se referir a outras partes do aplicativo.Esta é uma boa prática de design como faz alterações em uma parte menos suscetível para exigir alterações em outras.Para testar, permite que você substitua um stub para um componente real.
No diagrama StockAnalyzer, o componente é aquele que queremos teste.Normalmente usa outro componente, RealStockFeed.Mas RealStockFeed retorna os resultados diferentes cada vez que os métodos são chamados, tornando difícil testar StockAnalyzer.Durante testes, nós substituí-los com uma classe diferente, StubStockFeed.
Porque os stubs dependem do poder dessa maneira estruturar seu código, você normalmente usa stub para isolar de uma parte do seu aplicativo de outro.Para isolá-lo de outros assemblies que não está sob seu controle, como System.dll, você usaria normalmente correções.Consulte Usando shims para isolar seu aplicativo de outros assemblies para testes de unidade.
Requisitos
- Visual Studio Ultimate
Neste tópico
Como usar stub
Design para a inclusão de dependência
Gere stub
Escreva seu teste com stub
Verificando valores de parâmetro
Como usar stub
Design para a inclusão de dependência
Para usar stub, seu aplicativo tem que ser criado para que os diferentes componentes não são dependentes em si, mas somente dependente de definições de interface.Em vez de ser acoplado em tempo de compilação, os componentes são conectados em tempo de execução.Esse padrão ajuda a tornar o software que é robusto e fácil de atualizar, porque as alterações tendem a não se propagar através de limites componentes.Recomendamos depois mesmo se você não usar modelos.Se você estiver escrevendo o novo código, é fácil seguir injeção de dependência o padrão.Se você estiver escrevendo teste para o software existente, você talvez precise refactor ele.Se isso seria prático, você pode considerar o uso correções em vez disso.
Vamos começar com esta discussão um exemplo de motivação, que no diagrama.A classe StockAnalyzer ler aspas e produz resultados alguns interessantes.Tem alguns métodos públicos, que queremos teste.Para manter as coisas simples, vamos ter apenas um desses métodos, muito simples que relata o preço atual de um compartilhamento específico.Queremos escrever um teste de unidade do método.Aqui está o primeiro esboço de um teste:
[TestMethod]
public void TestMethod1()
{
// Arrange:
var analyzer = new StockAnalyzer();
// Act:
var result = analyzer.GetContosoPrice();
// Assert:
Assert.AreEqual(123, result); // Why 123?
}
<TestMethod()> Public Sub TestMethod1()
' Arrange:
Dim analyzer = New StockAnalyzer()
' Act:
Dim result = analyzer.GetContosoPrice()
' Assert:
Assert.AreEqual(123, result) ' Why 123?
End Sub
Um problema com esse teste é imediatamente óbvio: as aspas variam, e assim que a declaração falhará em geral.
Outro problema pode ser que o componente de StockFeed, que é usado pelo StockAnalyzer, ainda está em desenvolvimento.Aqui está o primeiro esboço de código do método no teste:
public int GetContosoPrice()
{
var stockFeed = new StockFeed(); // NOT RECOMMENDED
return stockFeed.GetSharePrice("COOO");
}
Public Function GetContosoPrice()
Dim stockFeed = New StockFeed() ' NOT RECOMMENDED
Return stockFeed.GetSharePrice("COOO")
End Function
Portanto, este método não pode compilar ou pode lançar uma exceção devido o trabalho na classe de StockFeed ainda não está completo.
A inclusão de interface aborda ambos esses problemas.
A inclusão de interface aplica a regra seguir:
Todo o código do componente do seu aplicativo nunca deve explicitamente referir-se a uma classe em outro componente, em uma declaração ou em uma declaração de new .Em vez disso, variáveis e parâmetros devem ser declarados com interfaces.Instâncias de componentes devem ser criados somente pelo contêiner do componente.
O component nesse caso queremos dizer uma classe, ou um grupo de classes que você desenvolver e atualiza juntos.Normalmente, um componente é o código em um projeto Visual Studio.É importante menos desacoplar classes de um componente, porque são atualizados ao mesmo tempo.
Não é tão também importante desacoplar seus componentes de classes de uma plataforma relativamente estável como System.dll.Interfaces para gravar todas essas classes desordenaria seu código.
O código de StockAnalyzer como consequência pode ser melhorado desacoplando da StockFeed usando uma interface como este:
public interface IStockFeed
{
int GetSharePrice(string company);
}
public class StockAnalyzer
{
private IStockFeed stockFeed;
public Analyzer(IStockFeed feed)
{
stockFeed = feed;
}
public int GetContosoPrice()
{
return stockFeed.GetSharePrice("COOO");
}
}
Public Interface IStockFeed
Function GetSharePrice(company As String) As Integer
End Interface
Public Class StockAnalyzer
' StockAnalyzer can be connected to any IStockFeed:
Private stockFeed As IStockFeed
Public Sub New(feed As IStockFeed)
stockFeed = feed
End Sub
Public Function GetContosoPrice()
Return stockFeed.GetSharePrice("COOO")
End Function
End Class
Nesse exemplo, StockAnalyzer é passado uma implementação de um IStockFeed quando é construído.No aplicativo concluído, o código de inicialização executaria a conexão:
analyzer = new StockAnalyzer(new StockFeed())
Há maneiras mais flexíveis de executar esta conexão.Por exemplo, StockAnalyzer pode aceitar um objeto de fábrica que pode criar uma instância diferentes implementações de IStockFeed em diferentes circunstâncias.
Gere stub
Você desacoplou a classe que você deseja testar outros componentes que usa.Assim como tornar mais robusto do aplicativo e flexível, desacoplar permite que você se conecte o componente no teste para arrancar implementações de interfaces para fins de teste.
Você pode simplesmente gravar os stubs como classes da maneira habitual.Mas as falsificação Microsoft fornece uma maneira mais dinâmica de criar um stub mais adequada para cada teste.
Para usar modelos, você deve primeiro gerar tipos de stub das definições de interface.
Adicionando um conjunto de falsificação
No solution Explorer, expanda Referênciasdo seu projeto de teste de unidade.
- Se você estiver trabalhando em Visual Basic, você deve selecionar Mostrar todos os arquivos na barra de ferramentas do Gerenciador de soluções, para ver a lista de referências.
Selecione o assembly que contém definições de interface para o qual você deseja criar modelos.
No menu de atalho, escolha Adicione o conjunto de falsificação.
Escreva seu teste com stub
[TestClass]
class TestStockAnalyzer
{
[TestMethod]
public void TestContosoStockPrice()
{
// Arrange:
// Create the fake stockFeed:
IStockFeed stockFeed =
new StockAnalysis.Fakes.StubIStockFeed() // Generated by Fakes.
{
// Define each method:
// Name is original name + parameter types:
GetSharePriceString = (company) => { return 1234; }
};
// In the completed application, stockFeed would be a real one:
var componentUnderTest = new StockAnalyzer(stockFeed);
// Act:
int actualValue = componentUnderTest.GetContosoPrice();
// Assert:
Assert.AreEqual(1234, actualValue);
}
...
}
<TestClass()> _
Class TestStockAnalyzer
<TestMethod()> _
Public Sub TestContosoStockPrice()
' Arrange:
' Create the fake stockFeed:
Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed
With stockFeed
.GetSharePriceString = Function(company)
Return 1234
End Function
End With
' In the completed application, stockFeed would be a real one:
Dim componentUnderTest As New StockAnalyzer(stockFeed)
' Act:
Dim actualValue As Integer = componentUnderTest.GetContosoPrice
' Assert:
Assert.AreEqual(1234, actualValue)
End Sub
End Class
A parte especial de mágica aqui é a classe StubIStockFeed.Para cada público digite no assembly referenciado, a evitar falsificação da Microsoft que o mecanismo gera uma classe de stub.O nome da classe stub é nomes derivados do nome da interface comFakes.Stub“,” como um prefixo, e o tipo de parâmetro anexados.
Os modelos são também gerados para o getter e definidores propriedades, eventos para, para e métodos genéricos.
Verificando valores de parâmetro
Você pode verificar que quando seu componente faz uma chamada para outro componente, passe valores corretos.Ou você pode colocar uma declaração no stub, ou você pode armazenar o valor e verifique o corpo do teste.Por exemplo:
[TestClass]
class TestMyComponent
{
[TestMethod]
public void TestVariableContosoPrice()
{
// Arrange:
int priceToReturn;
string companyCodeUsed;
var componentUnderTest = new StockAnalyzer(new StubIStockFeed()
{
GetSharePriceString = (company) =>
{
// Store the parameter value:
companyCodeUsed = company;
// Return the value prescribed by this test:
return priceToReturn;
};
};
// Set the value that will be returned by the stub:
priceToReturn = 345;
// Act:
int actualResult = componentUnderTest.GetContosoPrice(priceToReturn);
// Assert:
// Verify the correct result in the usual way:
Assert.AreEqual(priceToReturn, actualResult);
// Verify that the component made the correct call:
Assert.AreEqual("COOO", companyCodeUsed);
}
...}
<TestClass()> _
Class TestMyComponent
<TestMethod()> _
Public Sub TestVariableContosoPrice()
' Arrange:
Dim priceToReturn As Integer
Dim companyCodeUsed As String = ""
Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed()
With stockFeed
' Implement the interface's method:
.GetSharePriceString = _
Function(company)
' Store the parameter value:
companyCodeUsed = company
' Return a fixed result:
Return priceToReturn
End Function
End With
' Create an object to test:
Dim componentUnderTest As New StockAnalyzer(stockFeed)
' Set the value that will be returned by the stub:
priceToReturn = 345
' Act:
Dim actualResult As Integer = componentUnderTest.GetContosoPrice()
' Assert:
' Verify the correct result in the usual way:
Assert.AreEqual(priceToReturn, actualResult)
' Verify that the component made the correct call:
Assert.AreEqual("COOO", companyCodeUsed)
End Sub
...
End Class
Stub para diferentes tipos de membros tipo
Métodos
Conforme descrito no exemplo, os métodos podem ser arrancados anexando um representante para uma instância da classe stub.O nome do tipo de stub é derivado dos nomes dos parâmetros do método e.Por exemplo, dado a seguir interface de IMyInterface e MyMethodmétodo:
// application under test
interface IMyInterface
{
int MyMethod(string value);
}
Nós anexamos um stub para MyMethod que sempre retorna 1:
// unit test code
var stub = new StubIMyInterface ();
stub.MyMethodString = (value) => 1;
Se você não fornecer um stub para uma função, as falsificação gerarão uma função que retorna o valor padrão do tipo de retorno.Para números, o valor padrão é 0, e para tipos de classe é null (C#) ou Nothing (Visual Basic).
Propriedades
O getter e definidores de propriedade são expostos como representantes separados e podem ser arrancados separadamente.Por exemplo, considere a propriedade de Value de IMyInterface:
// code under test
interface IMyInterface
{
int Value { get; set; }
}
Nós anexamos representantes o getter e o setter de Value para simular uma propriedade automática:
// unit test code
int i = 5;
var stub = new StubIMyInterface();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;
Se você não fornecer métodos de stub para o definidor ou o getter de uma propriedade, as falsificação irão gerar um stub que armazena valores, de modo que a propriedade stub funciona como um variável simples.
Eventos
Os eventos são expostos como campos de representante.Como resultado, qualquer evento pode ser gerado arrancado simplesmente chamar o campo de backup do evento.Vamos considerar a seguir interface para arrancar:
// code under test
interface IWithEvents
{
event EventHandler Changed;
}
Para disparar o evento de Changed , chamamos simplesmente o representante de apoio:
// unit test code
var withEvents = new StubIWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);
Métodos genéricos
É possível arrancar métodos genéricos fornecendo um representante para cada instanciação desejada do método.Por exemplo, dado a seguir interface que contém um método genérico:
// code under test
interface IGenericMethod
{
T GetValue<T>();
}
você pode escrever um teste que arrancasse a instanciação de GetValue<int> :
// unit test code
[TestMethod]
public void TestGetValue()
{
var stub = new StubIGenericMethod();
stub.GetValueOf1<int>(() => 5);
IGenericMethod target = stub;
Assert.AreEqual(5, target.GetValue<int>());
}
Se o código era chamar GetValue<T> com qualquer outra instanciação, o stub chamaria simplesmente o comportamento.
Modelos de classes virtuais
Nos exemplos anteriores, os modelos foram gerados interfaces.Você também pode gerar modelos de uma classe que tem membros virtuais ou abstratos.Por exemplo:
// Base class in application under test
public abstract class MyClass
{
public abstract void DoAbstract(string x);
public virtual int DoVirtual(int n)
{ return n + 42; }
public int DoConcrete()
{ return 1; }
}
No stub gerado da classe, você pode definir métodos de representante para DoAbstract() e DoVirtual(), mas não DoConcrete().
// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;
Se você não fornecer um delegado para um método virtual, as falsificação ou podem fornecer comportamento padrão, ou pode chamar o método na classe base.Para ter o método base chamado, defina a propriedade de CallBase :
// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set – default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));
stub.CallBase = true;
//No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));
Stub de depuração
Os tipos de stub são criados para fornecer uma experiência plana de depuração.Por padrão, o depurador é instruído para entrar em todo o código gerado, portanto deve ir diretamente nas implementações personalizadas do membro que foram anexadas a stub.
Limitações de stub
As assinaturas de método com ponteiros não são suportadas.
As classes ou métodos selados estático não podem ser arrancados porque os tipos de stub dependem de distribuição virtual do método.Para esse caso, use tipos corretivos como descrito em Usando shims para isolar seu aplicativo de outros assemblies para testes de unidade
Alterar o comportamento padrão de stub
Cada tipo gerado stub contém uma instância da interface de IStubBehavior (através da propriedade de IStub.InstanceBehavior ).O comportamento é chamado sempre que um cliente chama um membro sem o delegado anexado personalizado.Se o comportamento não foi definido, usará a instância retornada pela propriedade de StubsBehaviors.Current .Por padrão, essa propriedade retorna um comportamento que lança uma exceção de NotImplementedException .
O comportamento pode ser alterado a qualquer momento definindo a propriedade de InstanceBehavior em qualquer instância de stub.Por exemplo, o seguinte trecho altera o comportamento que não faz nada ou retorna o valor padrão do tipo de retorno: default(T):
// unit test code
var stub = new StubIFileSystem();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;
O comportamento também pode ser alterado global para todos os objetos de stub para que o comportamento não foi configurado definindo a propriedade de StubsBehaviors.Current :
// unit test code
//change default behavior for all stub instances
//where the behavior has not been set
StubBehaviors.Current =
BehavedBehaviors.DefaultValue;