简体   繁体   中英

Database function Mock is not working in c# web api unit testing

Here is my controller method

[HttpPost]
    [Authorize]
    [Route(RouteConfig.Routes.LovList.contactStatus)]
    public IHttpActionResult ContactStatusList()
    {
        try
        {
            var result = new DBClass.HeroDb().GetList(
                            DBClass.DBConstants.ListConstants.query_Contact_Status);
            return Json(new Models.Response(
                        Models.ResponseMessages.Success,
                        result)
                     );
        }
        catch(System.Data.SqlClient.SqlException)
        {
            return InternalServerError();
        }
        catch(System.Exception ex)
        {
            Logger.Error(ex, ex.Message, ex.StackTrace);
            return InternalServerError();
        }
        
    }

and this is my test case method

[TestMethod()]
    public void ContactStatusListTest()
    {
        
        Mock<DBClass.HeroDb> mock = new Mock<DBClass.HeroDb>();
        mock.Setup(x => x.GetList(DBClass.DBConstants.ListConstants.query_Contact_Status))
            .Returns(CreateContactList());
        var result = new ListController().ContactStatusList();
        Models.Response response = (Models.Response)result;
        Assert.AreEqual(response.Message, Models.ResponseMessages.Success);
        Assert.IsNotNull(response.Data);
    }
    public System.Data.DataTable CreateContactList()
    {
        DataTable table = new DataTable();
        table.Columns.Add("ContactStatus");
        DataRow row1 = table.NewRow();row1["ContactStatus"] = "Contacted"; table.Rows.Add(row1);
        DataRow row2 = table.NewRow(); row2["ContactStatus"] = "Not Contacted"; table.Rows.Add(row2);
        DataRow row3 = table.NewRow(); row3["ContactStatus"] = "Contacted"; table.Rows.Add(row3);
        return table;
    }

I tried to mock GetList() function in my test method but it is not working. Controller method is giving an Internal server error . Because conrol is going to

var result = new DBClass.HeroDb()
            .GetList(DBClass.DBConstants.ListConstants.query_Contact_Status);

this line and db object is null here. Please help as i am beginner in unit test case building .

First of all let's set the basis first: unit testing is different than integration testing .

In that case this is a unit test on the controller's method ContactStatusList . You're testing only this method and you actually did things correctly by trying to mock your HeroDb object. Note that you decided to mock this object because this is a dependency .

The problem is you set up the Mock but you don't use it because in your ContactStatusList method you call new DBClass.HeroDb() .

There's a 2nd problem is that you're trying to mock a class. This is actually possible but all the class's methods you want to mock must be declared as virtual. Therefore it's actually better to mock an interface instead.

This interface should be received in the constructor of your ListController . On a regular execution of your Web Project inject an instance of that interface in the startup but in unit tests feed your mock to the ListController 's constructor.

Remember this rule: Any dependency should be received by your controller's constructor

Here is your interface and your DbHero class

public interface IDbHero
{
    IEnumerable<Contact> GetList(QueryContactStatus status);
}

public class DbHero : IDbHero
{
    public IEnumerable<Contact> GetList(QueryContactStatus status)
    {
        // Implementation here
    }
}

Now here's your controller:

[ApiController]
[Route("api/[controller]")]
public class ListController: ControllerBase
{
    private readonly IHeroDb _heroDb;

    public ListController(IHeroDb heroDb)
    {
        _heroDb = heroDb ?? throw new ArgumentNullException(nameof(heroDb));
    }
    
    [HttpPost]
    [Authorize]
    [Route(RouteConfig.Routes.LovList.contactStatus)]
    public IHttpActionResult ContactStatusList()
    {
        try
        {
            var result = _heroDb.GetList(DBClass.DBConstants.ListConstants.query_Contact_Status);
            return Json(new Models.Response(
                        Models.ResponseMessages.Success,
                        result)
                     );
        }
        catch(System.Exception ex)
        {
            Logger.Error(ex, ex.Message, ex.StackTrace);
            throw;
        }
    }
}

Note that I removed the block where you only catch the SqlException because anyway if you've an unhandled exception the server will return an internal server error so it's useless to catch it if you don't even log the error. Also in the 2nd catch block I just throw so the server will also automatically return an internal server error. If you're in debug mode this could be handy as you'd get the full Exception returned to you but if you return InternalServerError() you'd get no information even in debug and you'd have to check the logs...

In the ConfigureServices method of your Startup.cs class, inject your implementation of the IDbHero interface. Note this is a scoped service, meaning a new instance will be created for each HTTP request. Personally I never inject my Database access layer as a singleton because this could lead to some issues depending of the way this layer is implemented. For exemple EF Core's DbContext is incompatible with a singleton pattern.

services.AddScoped<IDbHero>(_ => new DBClass.HeroDb(Configuration.GetConnectionString("DbHeroConnectionString")));

I don't know how you handle the connection with the database because there's no mention of the connection string in your code example but I would do something like above.

Your connection string is coming from your appsettings.json config file

"ConnectionStrings": {
    "DbHeroConnectionString": "YourConnectionString"
  }

Now to use your mocked object in your unit test just do like this:

[TestMethod()]
public void ContactStatusList_ShouldReturnData_WhenCalled()
{
    // ARRANGE
    var mock = new Mock<IHeroDb>();
    mock.Setup(x => x.GetList(DBClass.DBConstants.ListConstants.query_Contact_Status))
        .Returns(CreateContactList());
    
    var sut = new ListController(mock.Object);
        
    // ACT
    var result = sut.ContactStatusList();
    
    // ASSERT
    Models.Response response = (Models.Response)result;
    Assert.AreEqual(response.Message, Models.ResponseMessages.Success);
    Assert.IsNotNull(response.Data);
}

Notice few things here:

  • Your unit test name: This should show 3 things:

    • What you're testing (your method)
    • What should be the result (a result with data, a result with error, an exception raised...etc)
    • Under which conditions this result should happen

    You can for exemple test that a method is returning an error when parameters have incorrect values. This should tested in another unit test

  • Unit tests are always in 3 parts ARRANGE , ACT and ASSERT . It's always a good practice to write that in each test so you can better organize your code

  • sut means System Under Test : this is what you want to test, all other dependencies (like your DbHero layer) should be mocked.

Now the next step would be to write a unit test to test your DbHero.GetList . This time you'll create a real instance (not a mock) of the DbHero class because this is what you want to test: this is your sut .

Note that I've an intermediate level in testing so what I'm showing you is good practices I've learnt from my coworkers. But maybe some more experience developers could come up with better practices than mine.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM