Reputation: 30862
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
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