How to unit test OData Client?
Asked Answered
K

1

11

I'm using Web Api OData v4 on the server and OData Client code generator on the client. It works fine, but I don't know how to test the code on the client.

On the server I expose a "Levels" dbSet.

Here's a snippet code on the client:

public class LevelViewer
{
   public virtual ODataContainer Container{get;set;} //t4 template generated

   public LevelViewer(ODataContainer container=null)
   {
       if(container==null)
       {
          Container=new ODataContainer(new Uri("http://blabla"));
       }
   }

   //I want to test this (actually there are more things, this is an example)
   public List<Level> GetRootLevels()
   {
       return ODataContainer.Levels.Where(l=>l.IsRoot).ToList();
   }
}

I'm accepting the odata container generated by the T4 template as a parameter for the constructor in order to be able to Mock it somehow.

Unit test, here's where I'm lost:

    [TestMethod]
    public void LevelsVMConstructorTest()
    {
        List<Level>levels=new List<Level>();
        levels.Add(new Level(){Id=1,LevelId=1,Name="abc",IsRoot=True});
        IQueryable<Level>levelsIQ=levels.AsQueryable<Level>();

        //?
        var odataContainerMock=new Mock<ODataContainer>();
        odataContainerMock.Setup(m=>m.Levels).Returns( I DON'T KNOW );


        //I want to get here
        LevelViewer lv = new LevelViewer(odataContainerMock.Object);
        Assert.IsTrue(lv.GetRootLevels().Any());
    }

So in this unit test I only want to test the logic inside the GetRootLevels method, I don't want to make an integration test or a self hosting service, I just want to test the method with in-memory data.

How do I mock the OData client generated class which is actually a DataServiceContext class?

I'm using Moq, but it can be anything, (free or at least included in VS professional edition)

Edit: Here's the implementation of ODataContainer (remember this is autogenerated by Odata client)

public partial class ODataContainer : global::Microsoft.OData.Client.DataServiceContext
{
    /// <summary>
    /// Initialize a new ODataContainer object.
    /// </summary>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    public ODataContainer(global::System.Uri serviceRoot) : 
            base(serviceRoot, global::Microsoft.OData.Client.ODataProtocolVersion.V4)
    {
        this.ResolveName = new global::System.Func<global::System.Type, string>(this.ResolveNameFromType);
        this.ResolveType = new global::System.Func<string, global::System.Type>(this.ResolveTypeFromName);
        this.OnContextCreated();
        this.Format.LoadServiceModel = GeneratedEdmModel.GetInstance;
        this.Format.UseJson();
    }
    partial void OnContextCreated();
    /// <summary>
    /// Since the namespace configured for this service reference
    /// in Visual Studio is different from the one indicated in the
    /// server schema, use type-mappers to map between the two.
    /// </summary>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    protected global::System.Type ResolveTypeFromName(string typeName)
    {
        global::System.Type resolvedType = this.DefaultResolveType(typeName, "WebServiceOData", "Constraint_Data_Feed.WebServiceOData");
        if ((resolvedType != null))
        {
            return resolvedType;
        }
        resolvedType = this.DefaultResolveType(typeName, "DAL.Models", "Constraint_Data_Feed.DAL.Models");
        if ((resolvedType != null))
        {
            return resolvedType;
        }
        return null;
    }
    /// <summary>
    /// Since the namespace configured for this service reference
    /// in Visual Studio is different from the one indicated in the
    /// server schema, use type-mappers to map between the two.
    /// </summary>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    protected string ResolveNameFromType(global::System.Type clientType)
    {
        global::Microsoft.OData.Client.OriginalNameAttribute originalNameAttribute = (global::Microsoft.OData.Client.OriginalNameAttribute)global::System.Linq.Enumerable.SingleOrDefault(global::Microsoft.OData.Client.Utility.GetCustomAttributes(clientType, typeof(global::Microsoft.OData.Client.OriginalNameAttribute), true));
        if (clientType.Namespace.Equals("Constraint_Data_Feed.WebServiceOData", global::System.StringComparison.Ordinal))
        {
            if (originalNameAttribute != null)
            {
                return string.Concat("WebServiceOData.", originalNameAttribute.OriginalName);
            }
            return string.Concat("WebServiceOData.", clientType.Name);
        }
        if (clientType.Namespace.Equals("Constraint_Data_Feed.DAL.Models", global::System.StringComparison.Ordinal))
        {
            if (originalNameAttribute != null)
            {
                return string.Concat("DAL.Models.", originalNameAttribute.OriginalName);
            }
            return string.Concat("DAL.Models.", clientType.Name);
        }
        return null;
    }
    /// <summary>
    /// There are no comments for Levels in the schema.
    /// </summary>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    [global::Microsoft.OData.Client.OriginalNameAttribute("Levels")]
    public global::Microsoft.OData.Client.DataServiceQuery<global::Constraint_Data_Feed.DAL.Models.Level> Levels
    {
        get
        {
            if ((this._Levels == null))
            {
                this._Levels = base.CreateQuery<global::Constraint_Data_Feed.DAL.Models.Level>("Levels");
            }
            return this._Levels;
        }
    }
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    private global::Microsoft.OData.Client.DataServiceQuery<global::Constraint_Data_Feed.DAL.Models.Level> _Levels;
    /// <summary>
    /// There are no comments for Levels in the schema.
    /// </summary>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    public void AddToLevels(global::Constraint_Data_Feed.DAL.Models.Level level)
    {
        base.AddObject("Levels", level);
    }
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
    private abstract class GeneratedEdmModel
    {
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
        private static global::Microsoft.OData.Edm.IEdmModel ParsedModel = LoadModelFromString();
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
        private const string Edmx = @"<edmx:Edmx Version=""4.0"" xmlns:edmx=""http://docs.oasis-open.org/odata/ns/edmx"">
  <edmx:DataServices>
    <Schema Namespace=""DAL.Models"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
      <EntityType Name=""Level"">
        <Key>
          <PropertyRef Name=""Id"" />
        </Key>
        <Property Name=""Id"" Type=""Edm.Int32"" Nullable=""false"" />
        <Property Name=""Name"" Type=""Edm.String"" Nullable=""false"" />
        <Property Name=""LevelId"" Type=""Edm.Int32"" />
        <NavigationProperty Name=""Sublevels"" Type=""Collection(DAL.Models.Level)"" />
        <NavigationProperty Name=""Machines"" Type=""Collection(DAL.Models.Machine)"" />
      </EntityType>
      <EntityType Name=""Machine"">
        <Key>
          <PropertyRef Name=""Id"" />
        </Key>
        <Property Name=""Id"" Type=""Edm.Int32"" Nullable=""false"" />
        <Property Name=""Name"" Type=""Edm.String"" Nullable=""false"" />
        <Property Name=""LevelId"" Type=""Edm.Int32"" />
        <NavigationProperty Name=""Level"" Type=""DAL.Models.Level"">
          <ReferentialConstraint Property=""LevelId"" ReferencedProperty=""Id"" />
        </NavigationProperty>
        <NavigationProperty Name=""Parts"" Type=""Collection(DAL.Models.Part)"" />
      </EntityType>
      <EntityType Name=""Part"">
        <Key>
          <PropertyRef Name=""Id"" />
        </Key>
        <Property Name=""Id"" Type=""Edm.Int32"" Nullable=""false"" />
        <Property Name=""Name"" Type=""Edm.String"" Nullable=""false"" />
        <NavigationProperty Name=""Machines"" Type=""Collection(DAL.Models.Machine)"" />
      </EntityType>
   </Schema>
   <Schema Namespace=""WebServiceOData"" xmlns=""http://docs.oasis-open.org/odata/ns/edm"">
    <EntityContainer Name=""ODataContainer"">
        <EntitySet Name=""Levels"" EntityType=""DAL.Models.Level"">
          <NavigationPropertyBinding Path=""Sublevels"" Target=""Levels"" />
        </EntitySet>
    </EntityContainer>
   </Schema>
   </edmx:DataServices>
 </edmx:Edmx>";


        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
        public static global::Microsoft.OData.Edm.IEdmModel GetInstance()
        {
            return ParsedModel;
        }
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
        private static global::Microsoft.OData.Edm.IEdmModel LoadModelFromString()
        {
            global::System.Xml.XmlReader reader = CreateXmlReader(Edmx);
            try
            {
                return global::Microsoft.OData.Edm.Csdl.EdmxReader.Parse(reader);
            }
            finally
            {
                ((global::System.IDisposable)(reader)).Dispose();
            }
        }
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.OData.Client.Design.T4", "2.4.0")]
        private static global::System.Xml.XmlReader CreateXmlReader(string edmxToParse)
        {
            return global::System.Xml.XmlReader.Create(new global::System.IO.StringReader(edmxToParse));
        }
    }
 }    
Kartis answered 4/12, 2015 at 18:26 Comment(9)
You could ensure to never receive a null DefaultContainer testing the classes that use this constructor. And then, change the parameter to use a interface (IDefaultContainer or something similar). Then you can test GetRootLevels by comparing the return object to be whatever you setup on your mock class.Map
Thanks, I'll give it a try.Kartis
For that to work I have to mock the Levels property which is a DataServiceQuery<T>, which only has getter. I'm still thinkingKartis
You could extract DefaultContainer.Levels into a protected virtual List<Level> getContainerLevels, override it in your unit test, then test that getRootLevels filters the non-root levels. https://mcmap.net/q/222697/-how-to-mock-static-methods-in-c-using-moq-frameworkAllison
@ATM Please add the implementation of DefaultContainer. Base on the code you've provided it seems that DefaultContainer.Levels is a static property(I believe the code suppose to be Container.Levels...). Based on the information that Levels is DbSet I think this answer might solve your problem.Abutilon
@Allison Thanks, I'll try thatKartis
@OldFox I have added DefaultContainer implementation, I renamed it to ODataContainerKartis
@Allison Levels is a DataServiceQuery that only has a getter, and in that getter it calls CreateQuery on the base classKartis
@ATM the C'tor of DataServiceQuery is private. You can't mock classes with private C'tor therefore I post an answer using MsFakes... BTW the link in previous comment would be the answer if Levels was DbSet instead of DataServiceQueryAbutilon
A
11

The C'tor of DataServiceQuery is private, therefore I couldn't mock it using Moq.

I used MsFakes as a free code weaving tool to solve this problem:

[TestMethod]
public void LevelsVMConstructorTest()
{
    using (ShimsContext.Create())
    {
        List<Level> levels = new List<Level>();
        levels.Add(new Level() { Id = 1, LevelId = 1, Name = "abc", IsRoot = true });
        var levelsIQ = levels.AsQueryable();

        var fakeDataServiceQuery = new System.Data.Services.Client.Fakes.ShimDataServiceQuery<Level>();

        fakeDataServiceQuery.ProviderGet = () => levelsIQ.Provider;
        fakeDataServiceQuery.ExpressionGet = () => levelsIQ.Expression;
        fakeDataServiceQuery.ElementTypeGet = () => levelsIQ.ElementType;
        fakeDataServiceQuery.GetEnumerator = levelsIQ.GetEnumerator;

        var defaultContainerMock = new Mock<DefaultContainer>();
        defaultContainerMock.Setup(m => m.Levels).Returns(fakeDataServiceQuery);

        LevelViewer lv = new LevelViewer(odataContainerMock.Object);
        Assert.IsTrue(lv.GetRootLevels().Any());

   }
}
Abutilon answered 7/12, 2015 at 22:16 Comment(6)
Thanks, I'll give it a try, I'll let you know the result.Kartis
This should be the answer but it seems that I need VS premium edition to enable MSFakes and I have Professional edition, I'll try to get a copy of premium edition and I'll make a test, I have upvoted this answerKartis
@ATM ah I didn't know this premium issue... You can add a virtual property which will expose IEnumerable<Level> then use it in your code:public virtual IEnumerable<Level> GetLevels{get{return Levels;}}. With this approach you don't need any code weaving tool. BTW I marked Levels as virtual method in my test...Abutilon
I also thought on that option, but it would nice not to have a method/property for each dbset, that's like abstracting all the odata container, similar to a repository, I'm expecting not to fall in thereKartis
I agree with you that adding the property isn't an ideal solution... However Levels is not a DbSet, it is a DataServiceQuery. If Levels was a DbSet you could use Moq and this answer might be the solution to your question....Abutilon
I'm still trying to get a copy of premium edition, and if in the meantime nothing else comes out, I'll mark your answerKartis

© 2022 - 2024 — McMap. All rights reserved.