混淆于DI、IoC、Unity和Moq的类型注册和实现方式。

4

我现在有一些空闲时间,正在努力理解DI和IoC容器。我选择了Unity,没有任何原因,仅仅是从我所知道的来看,各个主要框架之间没有太大的区别,我希望起步时不必过于担心。随着事情变得更加复杂,我意识到我可能需要改变,但现在我希望它能够胜任。

所以,我正在处理一个相对简单的数据访问场景,并实现了以下接口和数据访问类。

public interface IEventRepository
{
    IEnumerable<Event> GetAll();
}

public class EventRepository : IEventRepository
{
    public IEnumerable<Event> GetAll()
    {
        // Data access code here
    }
}

然后我可以按照以下步骤操作。
IUnityContainer container = new UnityContainer();
container.RegisterType(typeof(IEventRepository), typeof(EventRepository));

var eventRepo = container.Resolve<IEventRepository>();
eventRepo.GetAll();

如果在六个月内需要更改我的数据库提供商,从我的理解中我需要创建IEventRepository的一个新实现并更新类型注册,这很好。
现在,我感到困惑的地方是:例如,如果我想要实现一些缓存,我可以继承适当的IEventRepository实现,并覆盖适当的方法以实现必要的缓存。但是,这样做将使使用通过DI传递的Moq实现测试缓存是否正常工作变得更加困难,所以为了真正实践DI的精神,我认为创建一个IEventRepository实现,然后使用DI请求一个实际的数据访问IEventRepository实现会更有意义。
public class CachedEventRepository : IEventRepository
{
    private readonly IEventRepository _eventRepo;

    public CachedEventRepository(IEventRepository eventRepo)
    {
        if (eventRepo is CachedEventRepository)
            throw new ArgumentException("Cannot pass a CachedEventRepository to a CachedEventRepository");

        _eventRepo = eventRepo;
    }

    public IEnumerable<Event> GetAll()
    {
        // Appropriate caching code ultimately calling _eventRepo.GetAll() if needed
    }
}

这样做有意义吗?还是我做错了?你有什么建议?如果我做得正确,那么我该如何解决以下情况,以便CachedEventRepository获得适当的IEventRepository数据访问实现?

IUnityContainer container = new UnityContainer();
container.RegisterType(typeof(IEventRepository), typeof(EventRepository));
container.RegisterType(typeof(IEventRepository), typeof(CachedEventRepository));

var eventRepo = container.Resolve<IEventRepository>();
eventRepo.GetAll();

非常感谢您的帮助。

编辑1 以下是我希望能够执行的Moq测试,我认为这不可能使用继承来实现,需要使用DI。

var cacheProvider = new MemoryCaching();

var eventRepo = new Mock<IEventRepository>(MockBehavior.Strict);
eventRepo
    .Setup(x => x.GetAll())
    .Returns(() =>
    {
        return new Event[] { 
            new Event() { Id = 1}, 
            new Event() { Id = 2}
        };
    });

var cachedEventRepo = new CachedEventRepository(
    eventRepo.Object, 
    cacheProvider);

var data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
Assert.IsTrue(data.Count() > 0);
eventRepo.Verify(x => x.GetAll(), Times.Once());

// This set method should expire the cache so next time get all is requested it should
// load from the database again
cachedEventRepo.SomeSetMethod();

data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
Assert.IsTrue(data.Count() > 0);
eventRepo.Verify(x => x.GetAll(), Times.Exactly(2));

你不应该在eventRepo单元测试中测试缓存逻辑。这会违反SOLID原则中的单一职责思想。 - Anton Sizikov
我正在分别测试缓存和事件存储库。我想要测试的是将两者整合在一起,例如确保设置方法会使相应的缓存过期。 - Hawxby
是的,我明白了。现在我没有更多的想法了。 - Anton Sizikov
你认为我在我的“更新”中的变量怎么样? - Anton Sizikov
3个回答

2

好的,在这个问题上经过一些思考和对Unity的研究后,我想到了以下内容。

public class EventRepository : IEventRepository
{
    private readonly IDbManager _dbManager;

    public EventRepository(IDbManager dbManager)
    {
        _dbManager = dbManager;
    }

    public virtual IEnumerable<Event> GetAll()
    {
        // Data access code
    }
}

public class CachedEventRepository : IEventRepository
{
    private readonly ICacheProvider _cacheProvider;
    private readonly IEventRepository _eventRepo;

    public ICacheProvider CacheProvider
    {
        get { return _cacheProvider; }
    }

    public CachedEventRepository(IEventRepository eventRepo, ICacheProvider cacheProvider)
    {
        if(eventRepo is CachedEventRepository)
            throw new ArgumentException("eventRepo cannot be of type CachedEventRepository", "eventRepo");

        _cacheProvider = cacheProvider;
        _eventRepo = eventRepo;
    }

    public IEnumerable<Event> GetAll()
    {
        // Caching logic for this method with a call to _eventRepo.GetAll() if required
    }
}

这需要进行以下的 Unity 注册。对 IEventRepository 的解析请求将返回 CachedEventRepository。如果我想要快速删除缓存,只需删除 CachedEventRepository 的注册,就会恢复为 EventRepository。
IUnityContainer container = new UnityContainer();
container.RegisterType<IDbManager, SqlDbManager>();
container.RegisterType<ICacheProvider, MemoryCaching>();
container.RegisterType<IEventRepository, EventRepository>();
container.RegisterType<IEventRepository, CachedEventRepository>(
    new InjectionConstructor(
        new ResolvedParameter<EventRepository>(),
        new ResolvedParameter<ICacheProvider>())
    );

这样就可以得到我需要的测试。

一个简单的数据访问测试... SQL 是否正常工作。

IUnityContainer container = new UnityContainer();
container.RegisterType<IDbManager, SqlDbManager>();
container.RegisterType<EventRepository>();

var repo = container.Resolve<EventRepository>();

var data = repo.GetAll();

Assert.IsTrue(data.Count() > 0);

一个简单的缓存测试... 缓存系统是否正常工作
var cache = new MemoryCaching();

var getVal = cache.Get<Int32>(
    "TestKey",
    () => { return 2; },
    DateTime.UtcNow.AddMinutes(5));

Assert.AreEqual(2, getVal);

getVal = cache.Get<Int32>(
    "TestKey",
    () => { throw new Exception("This should not be called as the value should be cached"); },
    DateTime.UtcNow.AddMinutes(5));

Assert.AreEqual(2, getVal);

以下是两者配合使用的测试...个别方法的缓存是否按预期工作?缓存是否在应该过期时失效?方法参数是否正确地触发了新的数据库请求等。

var cacheProvider = new MemoryCaching();

var eventRepo = new Mock<IEventRepository>(MockBehavior.Strict);
eventRepo
    .Setup(x => x.GetAll())
    .Returns(() =>
    {
        return new Event[] { 
            new Event() { Id = 1}, 
            new Event() { Id = 2}
        };
    });

var cachedEventRepo = new CachedEventRepository(
    eventRepo.Object,
    cacheProvider);


cachedEventRepo.CacheProvider.Clear();
var data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
Assert.IsTrue(data.Count() > 0);
eventRepo.Verify(x => x.GetAll(), Times.Once());


cachedEventRepo.SomeSetMethodWhichExpiresTheCache();
data = cachedEventRepo.GetAll();
data = cachedEventRepo.GetAll();
Assert.IsTrue(data.Count() > 0);
eventRepo.Verify(x => x.GetAll(), Times.Exactly(2));

你认为这个怎么样?我认为它提供了良好的分离和良好的可测试性。

是的,这个比你的第一个版本好多了。 - Anton Sizikov

1

我认为你的CachedEventRepository是正确的方向,但我会将EventRepository的GetAll方法设置为虚方法,并让CachedEventRepository子类化EventRepoistory。然后子类可以重写GetAll方法,检查缓存,如果找不到则调用base.GetAll。然后它可以缓存结果并返回列表。

这样缓存逻辑就与数据访问逻辑分离了,子类将缓存行为添加到您的存储库中。

然后,您可以选择是否要使用缓存存储库,例如从配置文件中,并相应地配置Unity容器。

此外,您还可以为缓存服务创建一个接口,以便在单元测试CachedEventRepository时可以模拟它。


我唯一的担忧是这种方式无法传入依赖的EventRepository,因此我无法Moq一个并确保在缓存清除后的特定时间段内调用模拟方法x次。 - Hawxby
@Hawxby 我不确定我理解了。Cached子类采用了一个ICachingService接口。它取决于您对该接口的实现,以实际缓存正确的时间段。Cached存储库应完全不知道缓存是如何实际完成的;它应始终尝试从缓存中获取,如果未获取到任何内容,则应访问数据库。否则,您的Cached存储库正在执行两个操作;管理缓存以及利用缓存,这违反了关注点分离原则。 - Andy
看一下我对问题的修改,我认为这展示了我在测试中想要的东西。 - Hawxby

1
为什么不尝试将所有缓存逻辑封装在一个类中呢? 这样你就会得到类似这样的东西:
public interface ICacheManager {}

public class CacheManager : ICacheManager {}

因此,您可以编写所有单元测试以确保缓存逻辑正确。这将是CacheManagerTest类!

然后,您可以按照以下方式更改您的类:

public class EventRepository : IEventRepository
{
private ICacheManager _cacheManager;
public EventRepository(ICacheManager cacheManager)
{
    _cacheManager = cacheManager;
}
    public IEnumerable<Event> GetAll()
    {
        // Data access code here
    }
}

因此,在您的EventRepositoryTest类中不需要测试缓存逻辑,因为已经测试过了。

您可以设置IoC容器返回具有某些Cache策略参数的ICacheManager实例。

更新 好的,最后一次尝试:

public interface IEventRepo
{
    IEnumerable<Event> GetAll();
}

public interface ICacheProvider
{
    bool IsDataCached();
    IEnumerable<Event> GetFromCache();
}

public class CacheProvider : ICacheProvider
{

    public bool IsDataCached()
    {
        //do smth
    }

    public IEnumerable<Event> GetFromCache()
    {
        //get smth
    }
}


public class EventRepo : IEventRepo
{
    private ICacheProvider _cacheProvider;

    public EventRepo(ICacheProvider cacheProvider)
    {
     _cacheProvider = cacheProvider
    }

    public IEnumerable<Event> GetAll()
    {
        if (_cacheProvider.IsDataCached())
        {
            return _cacheProvider.GetFromCache();
        }
        else
        {
            //get from repo, save data in cache etc
        }
    }
}

[TestClass]
public class EventRepoTest
{
    [TestMethod]
    public void GetsDataFromCacheIfDataIsCachedTest()
    {
        var cacheProvider = new Mock<ICacheProvider>(MockBehavior.Strict);
        cacheProvider
            .Setup(x => x.IsDataCached())
            .Returns(() =>
            {
                return true;
            });
        cacheProvider
            .Setup(x => x.GetFromCache())
            .Returns(
            () => {
            return new Event[] { 
                new Event() { Id = 1}, 
                new Event() { Id = 2}
                };
            }
            );
        var eventRepo = new EventRepo(cacheProvider.Object);

        var data = eventRepo.GetAll();
        cacheProvider.Verify(x => x.GetFromCache(), Times.Once());
    }

    [TestMethod]
    public void GetsDataFromDataBaseIfNotCachedTest()
    {
        var cacheProvider = new Mock<ICacheProvider>(MockBehavior.Strict);
        cacheProvider
            .Setup(x => x.IsDataCached())
            .Returns(() =>
            {
                return false;
            });
        cacheProvider
            .Setup(x => x.GetFromCache())
            .Returns(
            () =>
            {
                return new Event[] { 
                new Event() { Id = 1}, 
                new Event() { Id = 2}
                };
            }
            );
        var eventRepo = new EventRepo(cacheProvider.Object);

        var data = eventRepo.GetAll();
        cacheProvider.Verify(x => x.GetFromCache(), Times.Never());
    }
}

不确定 Moq 语法,因为 WinPhone 没有 Moq,但我认为这不是问题。


我已经按照你的建议实现了一部分,创建了一个ICacheProvider和内存缓存实现。但是,我不想将其注入到EventRepository中,因为这会阻止对与缓存逻辑无关的EventRepository进行测试。 - Hawxby
在单元测试中,您可以注入一个简单的缓存提供程序,它只返回模拟数据或者什么都不做。这样您就可以避免对缓存逻辑进行双重测试。 - Anton Sizikov
这会在一定程度上对缓存逻辑进行双重测试。我不是直接测试缓存,也不是测试数据eventRepository,而是测试将两者结合起来创建cachedEventRepository的实现。因此,检查适当的方法是否自动过期缓存,导致缓存更新等。 - Hawxby
好的,我明白了。但是我认为在您的“编辑”中拥有测试并不是那么必要的。如果您已经对“CacheProvider”和“EventRepo”进行了充分的测试,并且“CachedEventRepository”中有独特的逻辑,那么只需测试该逻辑即可。无论如何,我会查看此页面以查看新的想法。 - Anton Sizikov
谢谢你的建议。今天早上我想到了另一个想法,并将其发布为可能的答案。我认为它涵盖了我所需要的一切,同时仍然提供了适当的分离。你觉得呢? - Hawxby

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接