m.edmondson
m.edmondson

Reputation: 30862

NSubstitute auto-mocking certain types only

I've noticed that NSubstitute auto-mocks the following types:

Although I know this happens, I can't find the reason for picking these types. Why not leave String null? Or include collections instead of just Array?

For example:

var crazyInterface = Substitute.For<ICrazyInterface>();

FileInfo[] folderContents = crazyInterface.FolderContents(@"C:\folder");
IObservable<FileInfo> fileObserver = crazyInterface.CreateNewFileObservable();
string fileHash = crazyInterface.FileHash(@"C:\folder\file.txt");
Task<byte[]> fileContents = crazyInterface.ReadFileContentsAsync(@"C:\folder\file.txt");

All get a mock implemented for them, whereas List<T> would be null (or any other reference type).

The source code gives no indication as to the reason just implements to auto-mocks in the AutoValues folder.

Upvotes: 0

Views: 710

Answers (1)

David Tchepak
David Tchepak

Reputation: 10464

Short answer: specific auto-mocked types just got added as people wanted them and we felt the convenience would outweigh any confusion caused by returning a real value when a mock or default(T) may have been expected.


In case anyone's interested here's the longer, hopefully-partially-accurate story of this feature's evolution as I recall it.

NSubstitute started off returning default(T) for everything. This gets painful for cases where we have chains of calls, so we added auto-mocking for interface types. This is a fairly safe operation - unlike with classes there is no potential for real code to run, say, from the constructor call, or by inadvertently calling a non-virtual method. For example, mySub.SomeAutoSub().DoStuff() could run real code or not based on the return type of SomeAutoSub().

The next level of safety is classes that have all virtual members, and a default constructor. The documentation refers to theses as pure virtual classes. Having virtual members means if we dig in to an auto-subbed instance we can't accidentally call real code. Now this will still run real code via the constructor, but it doesn't take parameters and so we can assume (read: cross-fingers and hope) the default behaviour will do something sensible rather than doing anything terrible.

Finally, there are the exceptional cases you pointed out like Array and Task. These were added completely ad hoc based on feedback when we felt the convenience would outweigh any confusion caused. The main thing we want to avoid with these is returning a real value when a mock was expected. NSubstitute doesn't work well if you accidentally call .Returns on something that isn't a mock.

Strings, Arrays and Tasks meet this criteria. You can't mock these types so having a default value that works sensibly in tests seems reasonable. I found I often ran into string ops that blew up because of the null default, so I added that one for convenience. Others wanted a default Task that worked sensibly and so added that. I can't remember the reasoning behind not auto-subbing collections (possibly because some people use mocked versions of IList<T>, where providing a specific implementation like List<T> is easier enough to explicitly stub?).

I think we can get away with some inconsistency here because we are in the context of tests -- if we need a specific value or behaviour we'll explicitly stub it out. Otherwise we'll get a default value that hopefully won't get in the way.

If you would like additional things auto-subbed please ping the discussion group. It's a bit harder to make potentially-breaking changes like this now but if there's a compelling case for a particular type we can look at adding it in.

Upvotes: 3

Related Questions