开发者

How can I use a mock when the code validates the types it receives

I want to test the following code:

public IEnumerable<KeyValuePair<Fact, Exception>> ValidateAll()
{
    //...do something
    var invalidFacts = GetInvalidFacts();
    //...do something

    return duplicateFacts.Concat(invalidFacts);
}

private IEnumerable<KeyV开发者_如何学GoaluePair<Fact, Exception>> GetInvalidFacts()
{
    var invalidFacts = Facts.Select(fact =>
    {
        try
        {
            fact.Validate();
            return new KeyValuePair<Fact, Exception>(fact, null);
        }
        catch (FormatException e)
        {
            return new KeyValuePair<Fact, Exception>(fact, e);
        }
        catch (Exception e)
        {
            return new KeyValuePair<Fact, Exception>(fact, e);
        }
    }).Where(kv => kv.Value != null).ToList();

    return invalidFacts;
}

Basically the test's objective is to verify that all objects that exist within the "Facts" IEnumerable will call their Validate method. Since I'm not interested to test the code within those objects, there are already lots of tests that do that, I want to inject a list of fake facts. I'm using MOQ to create the fakes.

So my unit test looks like this:

[TestMethod]
public void ValidateAll_ValidateMethodIsInvokedOnAllFacts_WhenCalled()
{
    var anyFactOne = new Mock<Fact>(); //Fact is an abstract class.

    anyFactOne.Setup(f => f.Validate());

    var dataWarehouseFacts = new DataWarehouseFacts { Facts = new Fact[] { anyFactOne.Object, FactGenerationHelper.GenerateRandomFact<SourceDetails>() } };

    dataWarehouseFacts.ValidateAll();
} 

Now I'm getting an exception because the code is actually validating the kind of Facts that can be injected to the DataWarehouseFacts class, like so:

public IEnumerable<Fact> Facts
{
    get
    {
        .....
    }
    set
    {
        var allowedTypes = new [] 
        { 
            typeof(ProductUnitFact), 
            typeof(FailureFact), 
            typeof(DefectFact), 
            typeof(ProcessRunFact), 
            typeof(CustomerFact),
            typeof(ProductUnitReturnFact),
            typeof(ShipmentFact),
            typeof(EventFact), 
            typeof(ComponentUnitFact),
            typeof(SourceDetails) 
        };

    if(!value.All(rootFact => allowedTypes.Contains(rootFact.GetType())))
       throw new Exception ("DataWarehouseFacts can only be set with root facts");

    ProductUnitFacts = value.OfType<ProductUnitFact>().ToList();
    FailureFacts = value.OfType<FailureFact>().ToList();
    DefectFacts = value.OfType<DefectFact>().ToList();
    ProcessRunFacts = value.OfType<ProcessRunFact>().ToList();
    CustomerFacts = value.OfType<CustomerFact>().ToList();
    ProductUnitReturnFacts = value.OfType<ProductUnitReturnFact>().ToList();
    ShipmentFacts = value.OfType<ShipmentFact>().ToList();
    EventFacts = value.OfType<EventFact>().ToList();
    ComponentUnitFacts = value.OfType<ComponentUnitFact>().ToList();
    SourceDetails = value.OfType<SourceDetails>().Single();
    }
}

What would be the best way to get around this validation?

Thanks.


The two obvious methods that leap to mind are:

  1. Add Fact to your list of allowed types.
  2. Moq one of your allowed fact types rather than the base Fact class itself. (I presume that your Validate() method is overrideable.)

Another slightly more complicated option would be to inject your list of allowed types at test time, assuming you have control over the DataWarehouseFacts class. That might look something like this:

class DWF
{
    static IEnumerable<Fact> defaultAllowedFacts = new Fact[] { ... }
    IEnumerable<Fact> allowedFacts;

    public DWF() : this(defaultAllowedFacts) { ... }
    internal DWF(IEnumerable<Fact> allowed)
    {
        // for testing only, perhaps
        this.allowedFacts = allowed;
    }
    ...
}

Then just delete that var allowedTypes = new [] bit and use this.allowedFacts instead.


I would leverage Type.IsAssignableFrom

E.g. instead of saying

allowedTypes.Contains(v.GetType())

I'd say

allowedTypes.Any(t => t.IsAssignableFrom(v.GetType()))

That way you can pass proper subclasses just as well as the exact matching types. Perhaps, maybe, that was what you were after with the typelist itself?


First of all I want to thank both ladenedge (I did gave a +1 to his answer) and sehe for their answers. Even though it was not exactly what I was looking for they are interesting ideas to keep in mind.

I couldn't just add the Fact class to the list of allowed types since that would have opened the door for lots of classes that should not be allowed; there are about 30 classes inheriting from it.

So what I ended up doing was to extract the code from the set part of the Facts property in their own methods and made one of them protected virtual, like so:

public IEnumerable<Fact> Facts
        {
            get
            {
                ...
            }
            set
            {
                ValidateReceived(value);
                ExtractFactTypesFrom(value.ToList());
            }
        }

        protected virtual void ValidateReceived(IEnumerable<Fact> factTypes)
        {
            if (factTypes == null) throw new ArgumentNullException("factTypes");

            var allowedTypes = GetAllowedFactTypes();
            if (!factTypes.All(rootFact => allowedTypes.Contains(rootFact.GetType()))) throw new Exception("DataWarehouseFacts can only be set with root facts");
        }

        private IEnumerable<Type> GetAllowedFactTypes()
        {
            var allowedTypes = new[]
            {
                typeof (ProductUnitFact), 
                typeof (SequenceRunFact), 
                typeof (FailureFact), 
                typeof (DefectFact),
                typeof (ProcessRunFact), 
                typeof (CustomerFact), 
                typeof (ProductUnitReturnFact),
                typeof (ShipmentFact), 
                typeof (EventFact), 
                typeof (ComponentUnitFact), 
                typeof (SourceDetails)
            };

            return allowedTypes;
        }

        private void ExtractFactTypesFrom(List<Fact> value)
        {
            ProductUnitFacts = value.OfType<ProductUnitFact>().ToList();
            FailureFacts = value.OfType<FailureFact>().ToList();
            DefectFacts = value.OfType<DefectFact>().ToList();
            ProcessRunFacts = value.OfType<ProcessRunFact>().ToList();
            SequenceRunFacts = value.OfType<SequenceRunFact>().ToList();
            CustomerFacts = value.OfType<CustomerFact>().ToList();
            ProductUnitReturnFacts = value.OfType<ProductUnitReturnFact>().ToList();
            ShipmentFacts = value.OfType<ShipmentFact>().ToList();
            EventFacts = value.OfType<EventFact>().ToList();
            ComponentUnitFacts = value.OfType<ComponentUnitFact>().ToList();
            SourceDetails = value.OfType<SourceDetails>().Single();
        }

That way I was able to create a DataWarehouseFactsForTest and override the ValidateReceived method so it wouldn't do anything:

public class DataWarehouseFactsForTests : DataWarehouseFacts
{
    protected override void ValidateReceived(IEnumerable<Fact> factTypes)
    {}
}

That way I was able to to use Moq to create the Facts and verify the code within the private GetInvalidFacts method. For example:

[TestMethod]
public void ValidateAll_ReturnsADictionaryWithAFormatException_WhenOneOfTheFactsValidationThrowsAFormatException()
{
    var anyFactOne = new Mock<ProductUnitFact>();
    var anyFactTwo = new Mock<SequenceRunFact>();
    var anyFactThree = new Mock<SourceDetails>();

    anyFactOne.Setup(f => f.Validate()).Throws(new FormatException());

    var dataWarehouseFacts = new DataWarehouseFactsForTests { Facts = new Fact[] { anyFactOne.Object, anyFactTwo.Object, anyFactThree.Object } };

    var result = dataWarehouseFacts.ValidateAll().ToList();

    anyFactOne.Verify(f => f.Validate(), Times.Exactly(1));
    anyFactTwo.Verify(f => f.Validate(), Times.Exactly(1));
    anyFactThree.Verify(f => f.Validate(), Times.Exactly(1));

    Assert.AreEqual(1, result.Count());
    Assert.AreEqual(typeof(FormatException), result.First().Value.GetType());
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜