单元测试许多调用内部方法/其他测试方法的简单方法,导致复杂(但相似)输出

时间:2017-04-07 09:55:36

标签: c# unit-testing testing

我的数据访问层有两种方法GetLatestXGetXGetLatestX看起来像这样:

    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. 开始收听消息
  3. 再次订阅不会导致重复的数据库调用
  4. 当模拟消息系统模拟消息时,将调用新的数据库访问,并且订阅将获得新版本。只使用了一个额外的数据库调用。
  5. 取消订阅第二个订阅并不会导致系统停止收听消息。
  6. 取消订阅第一个订阅会导致处理资源,并取消订阅消息。
  7. 所以,我在单个单元测试中得到了所有这些,这很可怕。但是,我不确定如何分解它。我只能测试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));
        }
    

0 个答案:

没有答案