How to test Singleton class that has a static depe

2019-09-01 04:58发布

问题:

I have a Singleton class that uses the thread-safe Singleton pattern from Jon Skeet as seen in the TekPub video. The class represents a cached list of reference data for dropdowns in an MVC 3 UI.

To get the list data the class calls a static method on a static class in my DAL.

Now I'm moving into testing an I want to implement an interface on my DAL class but obviously cannot because it is static and has only one static method so there's no interface to create. So I want to remove the static implementation so I can do the interface.

By doing so I can't call the method statically from the reference class and because the reference class is a singleton with a private ctor I can't inject the interface. How do I get around this? How do I get my interface into the reference class so that I can have DI and I can successfully test it with a mock?

Here is my DAL class in current form

public static class ListItemRepository {

    public static List<ReferenceDTO> All() {
        List<ReferenceDTO> fullList;
        ... /// populate list
        return fullList;
    }
}

This is what I want it to look like

public interface IListItemRepository {
    List<ReferenceDTO> All();
}

public class ListItemRepository : IListItemRepository {

    public List<ReferenceDTO> All() {
        List<ReferenceDTO> fullList;
        ... /// populate list
        return fullList;
    }
}

And here is my singleton reference class, the call to the static method is in the CheckRefresh call

public sealed class ListItemReference {

    private static readonly Lazy<ListItemReference> instance = 
        new Lazy<ListItemReference>(() => new ListItemReference(), true);

    private const int RefreshInterval = 60;
    private List<ReferenceDTO> cache;
    private DateTime nextRefreshDate = DateTime.MinValue;

    public static ListItemReference Instance {
        get { return instance.Value; }
    }

    public List<SelectListDTO> SelectList {
        get {
            var lst = GetSelectList();
            lst = ReferenceHelper.AddDefaultItemToList(lst);
            return lst;
        }
    }

    private ListItemReference() { }

    public ReferenceDTO GetByID(int id) {
        CheckRefresh();
        return cache.Find(item => item.ID == id);
    }

    public void InvalidateCache() {
        nextRefreshDate = DateTime.MinValue;
    }

    private List<SelectListDTO> GetSelectList() {
        CheckRefresh();
        var lst = new List<SelectListDTO>(cache.Count + 1);
        cache.ForEach(item => lst.Add(new SelectListDTO { ID = item.ID, Name = item.Name }));
        return lst;
    }

    private void CheckRefresh() {
        if (DateTime.Now <= nextRefreshDate) return;
        cache = ListItemRepository.All(); // Here is the call to the static class method
        nextRefreshDate = DateTime.Now.AddSeconds(RefreshInterval);
    }
    }
}

回答1:

You can use the singleton based on instance(not based on static), for which you can declare interface like this.

public interface IListItemRepository
{
    List<ReferenceDTO> All();
}


public class ListItemRepository : IListItemRepository
{
    static IListItemRepository _current = new ListItemRepository();

    public static IListItemRepository Current
    {
        get { return _current; }
    }

    public static void SetCurrent(IListItemRepository listItemRepository)
    {
        _current = listItemRepository;
    }

    public List<ReferenceDTO> All()
    {
        .....
    }
}

Now, you can mock IListItemRepository to test.

    public void Test()
    {
        //arrange
        //If Moq framework is used,
        var expected = new List<ReferneceDTO>{new ReferneceDTO()};

        var mock = new Mock<IListItemRepository>();           
        mock.Setup(x=>x.All()).Returns(expected);

        ListItemRepository.SetCurrent(mock.Object);

        //act
        var result = ListItemRepository.Current.All();

        //Assert
        Assert.IsSame(expected, result);
    }


回答2:

Which DI framework are you using? Depending on your answer, IOC container should be able to handle single-instancing so that you don't have to implement your own singleton pattern in the caching class. In your code you would treat everything as instanced classes, but in your DI framework mappings you would be able to specify that only one instance of the cache class should ever be created.



回答3:

One way to test it would be if you refactor your ListItemReference by adding extra property:

public sealed class ListItemReference {
    ...
    public Func<List<ReferenceDTO>> References = () => ListItemRepository.All();
    ...
    private void CheckRefresh() {
        if (DateTime.Now <= nextRefreshDate) return;
        cache = References();
        nextRefreshDate = DateTime.Now.AddSeconds(RefreshInterval);
    }
}

And then in your test you could do:

ListItemReference listReferences = new ListItemReference();
listReferences.References = () => new List<ReferenceDTO>(); //here you can return any mock data

Of course it's just temporary solution and I would recommend getting rid of statics by using IoC/DI.