简体   繁体   中英

Mocking out a specific method during a unit test

I'm relatively new to unit testing, and brand new to Moq. I have been tasked with writing up some unit tests for some pre-existing code.

I am struggling with the following method, as it makes a call to model.importing.RunJob , which as the name suggests, kicks off a job.

Obviously, I don't want it to actually DO the job, I just want to know that it's called the method.

How would I go about achieving this (ideally avoiding any code changes):

public ActionResult ImportsAndLoads(ImportsAndLoadsViewModel importsAndLoadsModel)
{
    Initialise(importsAndLoadsModel);
    LoadData(importsAndLoadsModel);

    if (importsAndLoadsModel.ActionToPerform == "RunJob")
    {
        using (var model = new Model())
        {
            List<Message> lstMessage;
            model.importing.RunJob((Jobs)Enum.ToObject(typeof(Jobs), importsAndLoadsModel.SelectedJob), out lstMessage);
            importsAndLoadsModel.lstMessages = lstMessage;
        }
    }
    return View(importsAndLoadsModel);
}

As has been discussed in the comments on your question, you can't really test your ImportsAndLoads method in isolation in its current form, using Moq. If you are using a high enough version of Visual Studio, then you may be able to test the method using Shims , however if you're not already using this in your project, it's probably not the right way to go.

As it stands, your code is a little confusing. It's a bit weird having a class called Model , that you're creating, in order to access a property in order to call a RunJob method. If this really is the code, then you might want to encourage the team to revisit the Model class.

One of the suggestions was that you inject your Model dependency instead of creating it (or inject a factory and call out to the factory for creation). This would be the preferable way to go, however it's not a trivial change in approach and really your team needs to buy into it as an approach rather than you making the change as a one-off.

If you aren't using an IOC container already (AutoFac, CastleWindsor, Ninject), then you may want to consider using one. They will make switching to dependency injection easier. If you want to do it by hand, then you can do, but it's harder.

Without knowing about the structure of more of your classes, it's hard to give a complete example, but one approach might be the following:

// Create an importer interface
public interface IImporter {
    void RunJob(Jobs job, out List<Message> listMessages);
}

// Implement it in a concrete class
public class Importer : IImporter{

    public void RunJob(Jobs job, out List<Message> listMessages) {
        // Do whatever it is it does.
    }
}

// Modify the constructor of your controller to allow the IImporter
// interface to be injected.  Default to null if not supplied, so that
// it can still be crated without parameters by the default MVC plumbing
public class ImportController : Controller
{
    private IImporter _importer;
    public ImportController(IImporter importer = null) {
        // If importer not injected, created a concrete instance
        if (null == importer) {
            importer = new Importer();
        }
        // Save dependency for use in actions
        _importer = importer;
    }

    // Use the injected importer in your action, rather than creating a model
    public ActionResult ImportsAndLoads(ImportsAndLoadsViewModel importsAndLoadsViewModel)
    {
        List<Message> listMsgs;
        _importer.RunJob(Jobs.One, out listMsgs);
        importsAndLoadsViewModel.lstMessages = listMsgs;
        return View(importsAndLoadsViewModel);
    }
}

This would then allow you to write a test to validate that importsAndLoadsViewModel has been updated as expected, using a test like this:

[Test]
public void TestModelMessagesAreUpdatedFromJobRunner() {
    var mockImporter = new Mock<IImporter>();
    List<Message> expectedMessages = new List<Message>();

    mockImporter.Setup(x=>x.RunJob(It.IsAny<Jobs>(), out expectedMessages));

    var model = new ImportsAndLoadsViewModel();

    // Inject the mocked importer while constructing your controller
    var sut = new ImportController(mockImporter.Object);

    // call the action you're testing on your controller
    ViewResult response = (ViewResult)sut.ImportsAndLoads(model);

    // Validate the the model has been updated to have the messages
    // returned by the mocked importer.
    Assert.AreEqual(expectedMessages, 
                    ((ImportsAndLoadsViewModel)response.Model).lstMessages);
}

This is simplification of what would need to be done / tested to demonstrate an approach. One of the issues to be aware of with injecting dependencies is that it's quite easy to end up creating abstractions of abstractions, pushing the actual logic deeper and deeper into your code, only to find that you've just pushed the problem deeper and that you still don't know how to test a particular piece of logic because in essence it hasn't changed.

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