我的数据访问层有两种方法GetLatestX
和GetX
。 GetLatestX
看起来像这样:
public IElementType GetLatestElementType(Guid id)
{
IElementType record = null;
using (DatabaseSession session = CreateSession())
{
record = session.Connection.Get<ElementTypeRecord>(id);
}
return record;
}
相当容易进行单元测试。
但是,GetX
将此GetLatest
包装在RefCount Observable中并发出新值以响应消息传递系统。测试此方法要复杂得多。我想检查以下内容,而不是复杂的行为:
所以,我在单个单元测试中得到了所有这些,这很可怕。但是,我不确定如何分解它。我只能测试1,但测试2我必须经历1和2,因为3我仍然需要经历步骤1,2,3等等所以我只是复制相同的巨型测试,但每次都在不同的地方进行断言。
我在这个方法中测试的代码:
public IObservable<IElement> GetElement(Guid id)
{
return CreateObservableFor(GetLatestElement(id), GetLatestElement);
}
它是一条线,其中一半已经过早期测试过。另一半是私人的:
private IObservable<T> CreateObservableFor<T>(T entity, Func<Guid, T> getLatest)
{
Guid id = (entity as ConfigurationEntity).ID;
//return subject;
return Observable.Create<T>(observer =>
{
// publish first value
observer.OnNext(entity);
// listen to internal or external update notifications from messages
Action<ConfigurationMessage> callback = (message) =>
{
// TODO, check time-stamp is after previous?
// use callback to get latest value
observer.OnNext(getLatest(id));
};
messageService.SubscribeToConfiguration(id.ToString(), callback);
// if it has been completed and stop listening to messages
return Disposable.Create(() =>
{
Console.WriteLine("Unsubscribing from topic " + id);
messageService.UnsubscribeToConfiguration(id.ToString(), callback);
});
}).Publish().RefCount();
}
但它对所有GetX
方法的行为方式相同。
我的第一个想法是我应该将GetLatestX
拆分为一个接口,我可以单独测试然后模拟 - 但这似乎将数据访问类拆分为两个,除了单元测试之外没有其他正当理由。在我看来,它们在概念上并不属于单独的单位。还有另一种嘲笑&#39;一个类中的这个依赖?或者我应该为了测试而拆分它们?
同样,测试GetX的功能有效地反复测试CreateObservableFor
的逻辑。我明白为什么我应该测试每个API方法而不是API的内部结构(如果有什么变化),但它似乎......效率低下。
如何以更好的方式构建此单元测试?
示例测试:
[Test]
public void GetElementTypeTest()
{
// test data
var id = Guid.NewGuid();
var nameA = "TestNameA";
var nameB = "TestNameB";
// mock database
var columnNames = new[] { "ID", "Name" };
// data values A will be the first set of data returned, and after configuration update B will be returned
var dataValuesA = new List<object[]>();
dataValuesA.Add(new object[] { id, nameA });
var dataValuesB = new List<object[]>();
dataValuesB.Add(new object[] { id, nameB });
mockDbProviderFactory = new MockDbProviderFactory()
.AddDatareaderCommand(columnNames, dataValuesA)
.AddDatareaderCommand(columnNames, dataValuesB);
// test method
IEMF emf = new EMF(mockMessageService.Object, new MockHistorian(), mockDbProviderFactory.Object, "");
var resultObservable = emf.GetElementType(id);
// check subscription to config changes has not occurred and database not accessed
mockDbProviderFactory.Verify(f => f.CreateConnection(), Times.Once);
mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Never);
//subscribe to observable
int sub1Count = 0;
var subscription = resultObservable.Subscribe(result => {
sub1Count++;
// check result
Assert.AreEqual(new ElementTypeRecord(id, (sub1Count == 1 ? nameA : nameB)), result, "Result from EMF does not match data");
});
// check subscribed to config changes and subscription called
Assert.IsTrue(sub1Count == 1, "Subscription not called");
mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);
// check we've subscribed with our id
Assert.AreEqual(this.configCallbacks[0].Item1, id.ToString(), "Unexpected message system subscription topic");
// open a second, short term subscription and ensure that the system does not re-subscribe to updates, or read the data again
int sub2Count = 0;
resultObservable.Take(1).Subscribe(result => {
sub2Count++;
// check result (should be second data item)
Assert.AreEqual(new ElementTypeRecord(id, nameB), result, "Result from EMF does not match data");
});
// check subscribed to config changes has not changed
mockMessageService.Verify(ms => ms.SubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);
//emit a new value by simulating a configuration change message
this.configCallbacks[0].Item2(new ConfigurationMessage(DateTime.Now));
// check subscriptions called
Assert.IsTrue(sub1Count == 2, "Subscription not called");
Assert.IsTrue(sub2Count == 1, "Subscription not called");
// unsubscribe
mockMessageService.Verify(ms => ms.UnsubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Never);
subscription.Dispose();
// verify subscription removed
mockMessageService.Verify(ms => ms.UnsubscribeToConfiguration(It.IsAny<string>(), It.IsAny<Action<ConfigurationMessage>>()), Times.Once);
Assert.IsTrue(this.configCallbacks.Count == 0, "Unexpected message system unsubscription topic");
// validate that the connection, command and reader were used correctly
mockDbProviderFactory.Verify(f => f.CreateConnection(), Times.Exactly(2));
mockDbProviderFactory.MockConnection.Verify(c => c.Open(), Times.Exactly(2));
mockDbProviderFactory.MockConnection.Verify(c => c.Close(), Times.Exactly(2));
//first data call
mockDbProviderFactory.MockCommands[0].Verify(c => c.PublicExecuteDbDataReader(It.IsAny<CommandBehavior>()), Times.Once);
mockDbProviderFactory.MockCommands[0].MockDatareader.Verify(dr => dr.Read(), Times.Exactly(2));
//second data call
mockDbProviderFactory.MockCommands[1].Verify(c => c.PublicExecuteDbDataReader(It.IsAny<CommandBehavior>()), Times.Once);
mockDbProviderFactory.MockCommands[1].MockDatareader.Verify(dr => dr.Read(), Times.Exactly(2));
}