Como faço para testar as assinaturas de agregador de eventos do Prism, no Uithread?
-
24-09-2019 - |
Pergunta
Eu tenho uma aula, que assina um evento via agregador de eventos do PRISMS.
Como é um pouco difícil de zombar do agregador de eventos, conforme observado aqui, Apenas instanciei um real e o passo para o sistema em teste.
No meu teste, publico o evento através desse agregador e verifiquei como meu sistema em teste reage a ele. Como o evento será aumentado por um FileSystemwatcher durante a produção, quero usar o despacho automático, assinando o Uithread, para que eu possa atualizar minha interface do usuário quando o evento for criado.
O problema é que, durante o teste, o evento nunca é notado no sistema em teste, a menos que eu não se inscreva no Uithread.
Estou usando o MSPEC para meus testes, que eu corro de dentro do vs2008 via tdd.net. Adicionando [RequiresSta]
para minha aula de teste não ajudou
Alguém tem uma solução, que me salva de mudar a opção Threads durante meus testes (por exemplo, por meio de uma propriedade - que hack que feio) ???
Solução
Se você zombar do evento e do agregador de eventos e usar o retorno de chamada do MOQ, poderá fazê -lo.
Aqui está um exemplo:
Mock<IEventAggregator> mockEventAggregator;
Mock<MyEvent> mockEvent;
mockEventAggregator.Setup(e => e.GetEvent<MyEvent>()).Returns(mockEvent.Object);
// Get a copy of the callback so we can "Publish" the data
Action<MyEventArgs> callback = null;
mockEvent.Setup(
p =>
p.Subscribe(
It.IsAny<Action<MyEventArgs>>(),
It.IsAny<ThreadOption>(),
It.IsAny<bool>(),
It.IsAny<Predicate<MyEventArgs>>()))
.Callback<Action<MyEventArgs>, ThreadOption, bool, Predicate<MyEventArgs>>(
(e, t, b, a) => callback = e);
// Do what you need to do to get it to subscribe
// Callback should now contain the callback to your event handler
// Which will allow you to invoke the callback on the test's thread
// instead of the UI thread
callback.Invoke(new MyEventArgs(someObject));
// Assert
Outras dicas
Eu realmente acho que você deveria usar zombarias para tudo e não para o EventAggregator. Não é difícil de zombar ... Não acho que a resposta vinculada prova muita coisa sobre a testabilidade do EventAggregator.
Aqui está o seu teste. Eu não uso o MSPEC, mas aqui está o teste no MOQ. Você não forneceu nenhum código, por isso estou baseando-o no código vinculado. Seu cenário é um pouco mais difícil do que o cenário vinculado, porque o outro OP só queria saber como verificar se a assinatura estava sendo chamada, mas você realmente quer chamar o método que foi aprovado na assinatura ... algo mais difícil, mas não muito.
//Arrange!
Mock<IEventAggregator> eventAggregatorMock = new Mock<IEventAggregator>();
Mock<PlantTreeNodeSelectedEvent> eventBeingListenedTo = new Mock<PlantTreeNodeSelectedEvent>();
Action<int> theActionPassed = null;
//When the Subscribe method is called, we are taking the passed in value
//And saving it to the local variable theActionPassed so we can call it.
eventBeingListenedTo.Setup(theEvent => theEvent.Subscribe(It.IsAny<Action<int>>()))
.Callback<Action<int>>(action => theActionPassed = action);
eventAggregatorMock.Setup(e => e.GetEvent<PlantTreeNodeSelectedEvent>())
.Returns(eventBeingListenedTo.Object);
//Initialize the controller to be tested.
PlantTreeController controllerToTest = new PlantTreeController(eventAggregatorMock.Object);
//Act!
theActionPassed(3);
//Assert!
Assert.IsTrue(controllerToTest.MyValue == 3);
Você pode não gostar disso, pois isso pode envolver o que você acha que é um "hack feio", mas minha preferência é usar um verdadeiro evento, em vez de zombar de tudo. Embora ostensivamente um recurso externo, o EventAGregreator é executado na memória e, portanto, não requer muita configuração, limpe e não é um pescoço de garrafa como outros recursos externos, como bancos de dados, serviços da web, etc. é apropriado para usar em um teste de unidade. Nessa base, usei esse método para superar o problema do encadeamento da interface do usuário no Nunit com alteração mínima ou risco para o meu código de produção em prol dos testes.
Em primeiro lugar, criei um método de extensão como assim:
public static class ThreadingExtensions
{
private static ThreadOption? _uiOverride;
public static ThreadOption UiOverride
{
set { _uiOverride = value; }
}
public static ThreadOption MakeSafe(this ThreadOption option)
{
if (option == ThreadOption.UIThread && _uiOverride != null)
return (ThreadOption) _uiOverride;
return option;
}
}
Então, em todas as minhas assinaturas de eventos, uso o seguinte:
EventAggregator.GetEvent<MyEvent>().Subscribe
(
x => // do stuff,
ThreadOption.UiThread.MakeSafe()
);
No código de produção, isso funciona perfeitamente. Para fins de teste, tudo o que preciso fazer é adicionar isso na minha configuração com um pouco de código de sincronização no meu teste:
[TestFixture]
public class ExampleTest
{
[SetUp]
public void SetUp()
{
ThreadingExtensions.UiOverride = ThreadOption.Background;
}
[Test]
public void EventTest()
{
// This doesn't actually test anything useful. For a real test
// use something like a view model which subscribes to the event
// and perform your assertion on it after the event is published.
string result = null;
object locker = new object();
EventAggregator aggregator = new EventAggregator();
// For this example, MyEvent inherits from CompositePresentationEvent<string>
MyEvent myEvent = aggregator.GetEvent<MyEvent>();
// Subscribe to the event in the test to cause the monitor to pulse,
// releasing the wait when the event actually is raised in the background
// thread.
aggregator.Subscribe
(
x =>
{
result = x;
lock(locker) { Monitor.Pulse(locker); }
},
ThreadOption.UIThread.MakeSafe()
);
// Publish the event for testing
myEvent.Publish("Testing");
// Cause the monitor to wait for a pulse, but time-out after
// 1000 millisconds.
lock(locker) { Monitor.Wait(locker, 1000); }
// Once pulsed (or timed-out) perform your assertions in the real world
// your assertions would be against the object your are testing is
// subscribed.
Assert.That(result, Is.EqualTo("Testing"));
}
}
Para tornar a espera e a pulsação mais sucintas, também adicionei os seguintes métodos de extensão às Threadingextensions:
public static void Wait(this object locker, int millisecondTimeout)
{
lock (locker)
{
Monitor.Wait(locker);
}
}
public static void Pulse(this object locker)
{
lock (locker)
{
Monitor.Pulse(locker);
}
}
Então eu posso fazer:
// <snip>
aggregator.Subscribe(x => locker.Pulse(), ThreadOption.UIThread.MakeSafe());
myEvent.Publish("Testing");
locker.Wait(1000);
// </snip>
Novamente, se suas sensibilidades significam que você deseja usar zombarias, vá em frente. Se você preferir usar a coisa real, isso funciona.