Reputation: 704
I have the following class, which uses the HTML Agility Pack to download a file. The GetImageUrls method has some logic that I would like to Unit Test, via Moq (https://github.com/moq/moq):
public class AgilityPackHtmlDownloadService : IHtmlDownloadService
{
private HtmlWeb _htmlWeb;
public AgilityPackHtmlDownloadService() : this(new HtmlWeb())
{
}
public AgilityPackHtmlDownloadService(HtmlWeb htmlWeb)
{
_htmlWeb = htmlWeb;
}
public IEnumerable<string> GetImageUrls(string pageUrl)
{
if (string.IsNullOrWhiteSpace(pageUrl))
{
throw new System.Exception("The Page URL cannot be blank");
}
var document = _htmlWeb.Load(pageUrl);
var result = new List<string>();
foreach (var element in document.DocumentNode.Descendants("img"))
{
var src = element.GetAttributeValue("src", null);
var dataSrc = element.GetAttributeValue("data-src", null);
var imageUrl = src ?? dataSrc;
if (!string.IsNullOrWhiteSpace(imageUrl))
{
result.Add(imageUrl);
}
}
return result;
}
}
The HtmlWeb class is not based on an interface, hence the reason for the 2 constructors – I want to inject a mock instance of HtmlWeb via the 2nd constructor.
I need to mock the Load method of HtmlWeb, which is defined as "public", but it is not virtual, so Moq can't handle it.
I've therefore created this class:
public class HtmlWebEx : HtmlWeb
{
public virtual new HtmlDocument Load(string url)
{
return base.Load(url);
}
}
Because this subclass has a virtual method called Load, it can be mocked via Moq like this:
var mockHtmlWeb = new Mock<HtmlWebEx>();
mockHtmlWeb.Setup(x => x.Load(It.IsAny<string>())).Returns(mockHtmlDocument);
IHtmlDownloadService htmlDownloadService = new AgilityPackHtmlDownloadService(mockHtmlWeb.Object);
Because the class under test expects a HtmlWeb parameter in the constructor, rather than a HtmlWebEx, the _htmlWeb.Load executes the base class' Load method, bypassing my Mock implementation.
(If I was using a standard virtual/override approach, it would be the subclasses's Load method that would be called, but I'm not overriding, just hiding).
How do I make the code call my subclass's mocked Load method when running the Unit Test?
I tried the following code for the constructors i.e. changing the type of the parameter, which does work, but now I've modified the code just to make the Unit Tests work, which doesn't seem correct:
public AgilityPackHtmlDownloadService() : this(new HtmlWebEx()) { //... }
public AgilityPackHtmlDownloadService(HtmlWebEx htmlWeb)
{
_htmlWeb = htmlWeb;
}
Upvotes: 0
Views: 69
Reputation: 301
Create an interface:
public interface IHtmlWeb
{
HtmlDocument Load(string url);
}
Create a class that subclasses the HtmlWeb class and declares that it implements the interface:
public class HtmlWebInterfaced : HtmlWeb, IHtmlWeb { }
Inject the implementation of the interface / mock:
public class AgilityPackHtmlDownloadService : IHtmlDownloadService
{
private readonly IHtmlWeb _htmlWeb;
public AgilityPackHtmlDownloadService(IHtmlWeb htmlWeb)
{
_htmlWeb = htmlWeb;
}
// rest of class removed for brevity...
}
In use, calls to the Load method will be handled by the method on the base class.
Upvotes: 2