Skip to content

Make the test library open for extension #597

Closed

Description

@brooknovak

I want to be able to have the mock file system to block tasks when writing to some streams, and sometime fails mid-writing streams. Plus all sorts of other things i wont to control.
It would be really easy to do if this awesome test library followed the open-closed SOLID principle.

Solution
Use virtual methods and properties so we can override default mocking behavior (or just extend it).

Activity

  1. issue-label-bot commented on Jun 2, 2020

    @issue-label-bot

    Issue-Label Bot is automatically applying the label type: enhancement to this issue, with a confidence of 0.86. Please mark this comment with 👍 or 👎 to give our bot feedback!

    Links: app homepage, dashboard and code for this bot.

  2. fgreinacher commented on Jun 12, 2020

    @fgreinacher
    Contributor

    Thanks for your suggestion! I think this is very similar to #288, right?

  3. added
    area: coreIssues that address the core abstractions & the wrappers
    on Jun 12, 2020
  4. brooknovak commented on Jun 12, 2020

    @brooknovak
    Author

    Yes it is the same request essentially.

    I have had to resort to using moq as well, which is not easy to do because you can’t just switch in a moq for 1 part.

    Virtual methods and properties on the test implementations members that implement the interface would be simplist to achieve extensibility for testing IMO.

  5. fgreinacher commented on Jun 16, 2020

    @fgreinacher
    Contributor

    Thanks for your feedback. As mentioned in the linked issue I'm hesitant against making stuff virtual too quickly. Could you provide some code samples that you think are currently harder than necessary?

  6. brooknovak commented on Jun 18, 2020

    @brooknovak
    Author

    Sure, what is your guide to know when something should be virtual?

    IMO I think making something virtual that is implementing a public interface method/property is relatively harmless.

    Making a method/property that is not part of a public interface (protected / some other public members) is dangerous. It exposes internal implementation detail for extension which is risky to do for a public library like this.

    Here is an example for extend the file system with moq:

    public class MoqFileSystem : IFileSystem, IMockFileDataAccessor
        {
            private readonly MockFileSystem _mockedFileSystem;
    
            public MoqFileSystem()
            {
                var currentDirectory = System.IO.Directory.GetCurrentDirectory();
                _mockedFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>(), currentDirectory);
                MockFile = new Mock<MockFile>(this) {CallBase = true};
                MockDirectory = new Mock<MockDirectory>(this, currentDirectory) { CallBase = true };
                MockFileInfo = new Mock<MockFileInfoFactory>(this) { CallBase = true };
                MockFileStream = new Mock<MockFileStreamFactory>(this) { CallBase = true };
                MockPath = new Mock<MockPath>(this) { CallBase = true };
                MockDirectoryInfo = new Mock<MockDirectoryInfoFactory>(this) { CallBase = true };
                MockDriveInfo = new Mock<MockDriveInfoFactory>(this) { CallBase = true };
                MockFileSystemWatcher = new Mock<MockFileSystemWatcherFactory>(this) { CallBase = true };
            }
    
            public Mock<MockFile> MockFile { get; }
            public Mock<MockDirectory> MockDirectory { get; }
            public Mock<MockFileInfoFactory> MockFileInfo { get; }
            public Mock<MockFileStreamFactory> MockFileStream { get; }
            public Mock<MockPath> MockPath { get; }
            public Mock<MockDirectoryInfoFactory> MockDirectoryInfo { get; }
            public Mock<MockDriveInfoFactory> MockDriveInfo { get; }
            public Mock<MockFileSystemWatcherFactory> MockFileSystemWatcher { get; }
    
            #region IFileSystem implementation
    
            public IFile File => MockFile.Object;
            public IDirectory Directory => MockDirectory.Object;
            public IFileInfoFactory FileInfo => MockFileInfo.Object;
            public IFileStreamFactory FileStream => MockFileStream.Object;
            public IPath Path => MockPath.Object;
            public IDirectoryInfoFactory DirectoryInfo => MockDirectoryInfo.Object;
            public IDriveInfoFactory DriveInfo => MockDriveInfo.Object;
            
            public IFileSystemWatcherFactory FileSystemWatcher => MockFileSystemWatcher.Object;
    
            #endregion
    
            #region IMockFileDataAccessor implementation
    
            public IFileSystem FileSystem => this;
    
            public PathVerifier PathVerifier => _mockedFileSystem.PathVerifier;
    
            public MockFileData GetFile(string path)
            {
                return _mockedFileSystem.GetFile(path);
            }
    
            public void AddFile(string path, MockFileData mockFile)
            {
                _mockedFileSystem.AddFile(path, mockFile);
            }
    
            public void AddDirectory(string path)
            {
                _mockedFileSystem.AddDirectory(path);
            }
    
            public void AddFileFromEmbeddedResource(string path, Assembly resourceAssembly, string embeddedResourcePath)
            {
                _mockedFileSystem.AddFileFromEmbeddedResource(path, resourceAssembly, embeddedResourcePath);
            }
    
            public void AddFilesFromEmbeddedNamespace(string path, Assembly resourceAssembly, string embeddedResourcePath)
            {
                _mockedFileSystem.AddFilesFromEmbeddedNamespace(path, resourceAssembly, embeddedResourcePath);
            }
    
            public void MoveDirectory(string sourcePath, string destPath)
            {
                _mockedFileSystem.MoveDirectory(sourcePath, destPath);
            }
    
            public void RemoveFile(string path)
            {
                _mockedFileSystem.RemoveFile(path);
            }
    
            public bool FileExists(string path)
            {
                return _mockedFileSystem.FileExists(path);
            }
    
            public IEnumerable<string> AllPaths => _mockedFileSystem.AllPaths;
            public IEnumerable<string> AllFiles => _mockedFileSystem.AllFiles;
            public IEnumerable<string> AllDirectories => _mockedFileSystem.AllDirectories;
            public StringOperations StringOperations => _mockedFileSystem.StringOperations;
    
            #endregion
        }
    

    I actually wrote a new file sytem that decorates the MockFile system in a similar way but not with moq, rather its a Fake that allows us to control things like hanging async file writes/reads.
    The example above, and the fake, would be much better off with virtual methods - we just want to extend the default mock implementation.

  7. fgreinacher commented on Jun 20, 2020

    @fgreinacher
    Contributor

    Sure, what is your guide to know when something should be virtual?

    See https://github.com/fgreinacher/opinions/blob/master/tech/virtuals-in-csharp.md (your questions actually was the trigger for me to finally write this down, thanks 👍)

    Here is an example for extend the file system with moq:

    Thanks a lot! So I assume the only thing that's missing to make your scenario a bit easier is to make the properties of MockFileSystem virtual? All other Mock* types should already have everything virtual because they derive from the *Base classes.

  8. brooknovak commented on Jun 21, 2020

    @brooknovak
    Author

    I like what you wrote on virtuals - totally agree with each point.

    Now I think about it - overriding parts of "Default" implementation that provides a base class is a bit odd: its troublesome because you don't know which combinations of things to override, and whether or not to continue to call the base implementation. Especially if the component is stateful.

    In all the cases I want to extend this library for, I essentially need hooks for certain operations to run extra code - in additional to the default behaviour. The decorator pattern on certain components would help here - however its not easy to do because the mock file system closes off the construction of all of the mocked components.

    Here is a strawman idea (without really thinking about it too hard): maybe if there was a way to make the mock file system construction extensible. Like some kind of factory method, or builder pattern. It might even pass a default instance that could be decorated. I need to provide an example but its too late in the night :p

  9. fgreinacher commented on Jun 23, 2020

    @fgreinacher
    Contributor

    Here is a strawman idea (without really thinking about it too hard): maybe if there was a way to make the mock file system construction extensible. Like some kind of factory method, or builder pattern. It might even pass a default instance that could be decorated. I need to provide an example but its too late in the night :p

    👍 I think something like #442 (and also #168) would work out pretty well.

  10. github-actions commented on Aug 23, 2020

    @github-actions

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues that address the core abstractions & the wrappersstate: needs discussionIssues that need further discussionstate: staletype: enhancementIssues that propose new functionality

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions