0

Am I correct to think that I have to create my controller by passing it an instance of my context AND my Service to make it testable?

For example: new Controller(mycontext,myservice)

I'm thinking this is the way I need to change my code, but I don't want to if I don't have to. Since for MVC3 to work out of the box it requires controller constructors to be parameterless, I think this means I'm going to have to go down the path of IoC. Otherwise the code in my Wizard action saves to a real DBContext even during testing.

namespace mvc3test.Controllers
{

    public class WizardController : Controller
    {

        private DR405DBContext db;


        public WizardController(DR405DBContext dbContext)
        {
            db = dbContext;
        }

        public WizardController()
        {
            db = new DR405DBContext();
        }

        public ActionResult Index()
        {
            var model = new WizardViewModel();
            model.Initialize();
            return View(model);
        }

        [HttpPost]
        public ActionResult Index([Deserialize] WizardViewModel wizard)
        {


            //wizard.Steps[wizard.CurrentStepIndex] = step;
            if (ModelState.IsValid)
            {

                    //Always save.
                    var obj = new dr405();

                    //wire up to domain model;
                    foreach (var s in wizard.Steps)
                    {
                        Mapper.Map(s,obj,s.GetType(), typeof(dr405));
                    }

                    using (var service = new DR405Service())
                    {
                        //Do something with a service here.
                        service.Save(db, obj);
                    }


                if (!string.IsNullOrEmpty(Request.QueryString["next"]))
                {
                    wizard.CurrentStepIndex++;
                }
                else if (!string.IsNullOrEmpty(Request.QueryString["prev"]))
                {
                    wizard.CurrentStepIndex--;
                }
                else
                {
                    return View("Review", wizard);

                }
            }
            else if (!string.IsNullOrEmpty(Request.QueryString["prev"]))
            {
                wizard.CurrentStepIndex--;
            }

            return View(wizard);


        }

        public ActionResult Review(int id)
        {
            var service = new DR405Service();

            var dr405 = service.GetDR405ById(db, id);
            var wizard = new WizardViewModel();

            if (dr405 != null)
            {

                wizard.Initialize();

                foreach (var s in wizard.Steps)
                {
                    Mapper.Map(dr405, s, typeof(dr405), s.GetType());
                }
            }

            return View(wizard);
        }


        public ActionResult Transmit()
        {
            return View();

        }

        [HttpPost]
        public String Upload(HttpPostedFileBase FileData)
        {
            var saveLocation = Path.Combine(Server.MapPath("\\"), "returns\\" + DR405Profile.CurrentUser.TaxPayerID);
            System.IO.Directory.CreateDirectory(saveLocation);
            FileData.SaveAs(Path.Combine(saveLocation, FileData.FileName));
            ViewBag.Message = String.Format("File name: {0}, {1}Kb Uploaded Successfully.", FileData.FileName, (int)FileData.ContentLength / 1024);
            return ViewBag.Message;
        }

    }
}
Doug Chamberlain
  • 11,192
  • 9
  • 51
  • 91
  • So, now I just pushed the issue down.... In my controller action I have `service.Save(obj)` now I need my DBContext to be `Generic` somehow. – Doug Chamberlain Jul 07 '11 at 19:51

2 Answers2

2

Am I correct to think that I have to create my controller by passing it an instance of my context AND my Service to make it testable?

Kind of. That's only half of the work you need to do. The second half is to weaken the coupling between your layers by using abstractions. Your service layer needs to implement an interface which you would inject into the constructor of your controller enforcing the contract between those two and explicitly stating that the controller needs a service layer obeying this contract:

public WizardController(IMyService service) 
{
   this._service = service;
}

Now in your unit test go ahead and mock it using one of the multiple mocking frameworks out there (Rhino Mocks, NSubstitute, Moq, NMock, ...).

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • that will fail to execute in default MVC3, since controllers must be parameterless. This is where I'm scared. This project is rapidly getting very large, Moq, FoolProof, Uploadify, etc.... I don't want to go adding things if I don't have to. Like IoC – Doug Chamberlain Jul 07 '11 at 18:41
  • @Doug Chamberlain, not only in ASP.NET MVC 3 controllers need to be parameterless. In all previous versions as well. That's the reason why you should use a DI framework such as [Ninject](http://ninject.org/), or any other, it doesn't really matter which one. Just pick one you like and have fun with it. The way you should design your controllers, and by the way that's valid for absolutely any .NET application, not only MVC, is by using weak coupling between your layers by introducing abstractions. That's the only way to be able to unit test different parts of your application in isolation. – Darin Dimitrov Jul 07 '11 at 18:42
  • 1
    Also note that there are also other type of developers: those who say that they don't care about design patterns and unit testing and simply do it the way they know it. And it just works. Two different schools, both with their pros and cons. Up to you to decide which philosophy you like and suits you best. I am not here to impose you anything. Just saying how in my experience I think is the correct way to develop applications that are reusable and easily maintainable. – Darin Dimitrov Jul 07 '11 at 18:46
  • good point on the philosophy. I know could get away with just slapping together something that works, but I really like being able to be involved in the development community, which requires using some standards..And I've always wanted to master unit testing...so DI framework here I come! I've gotten very sick of clicking through a website 10,000,000 times to test it. – Doug Chamberlain Jul 07 '11 at 19:05
  • Also, what do you think of your code LOL? I really found the wizard model you had come up with very useful. – Doug Chamberlain Jul 07 '11 at 19:06
  • see my comment above can you point me to a resource that will help with my second issue? Now my dbcontext has a save method which is called from the service, thus tightly coupling the two with `context.entity?????` which has to be strongly typed so I just pushed my problem horizontally – Doug Chamberlain Jul 08 '11 at 14:43
  • @Doug Chamberlain, your service should not be tightly coupled to a data context. It should use a repository abstraction interface. It's the repository implementation that would depend on the data context. And this one cannot be unit tested in isolation as it would depend on a database. That would be integration testing. – Darin Dimitrov Jul 08 '11 at 16:01
  • Right agreed. So I can have a repository that I can pass any class that implements my `Icontext` interface for lack of a better name? Then I can won't have to mock my repository. I'll instead mock the context that I pass to the repository So I could have a dbcontext and a fakeinmemorycontext that both implement Icontext. So now I can test? – Doug Chamberlain Jul 08 '11 at 16:59
1

You can use setter injection instead of constructor injection on the Controller.

public class WizardController : Controller
{
    public void setDBContext( DR405DBContext db) {
          this.db = db;
    }
}

or

You can get the database using a Service Locator and add a setter to that.

public class DBServiceLocator
{
    private static DR405DBContext db = new DR405DBContext();

    public static DR405DBContext locate() {
          return db;
    }

    public static setContext(DR405DBContext db) {
          DBServiceLocator.db = db;
    }
}

In the setup() part of your unit test, use setters to 'inject' your mock/stub database.

Also, using an interface rather than DR405DBContext will make mocking easier.

Garrett Hall
  • 29,524
  • 10
  • 61
  • 76