Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a utilities class to help write the boiler plate recording code a bit easier #83

Open
CableZa opened this issue Jun 18, 2020 · 8 comments

Comments

@CableZa
Copy link

CableZa commented Jun 18, 2020

Split from #79

While it's easy enough to contain this code in your own methods, it would be nice to have a utils type class that will help jump start users new to this library.

For this I propose adding an Utilties class.

@blairconrad
Copy link
Owner

Interesting idea, @CableZa. Can you tell me more? Without knowing how the interface would look, it's unclear what benefit it brings. Can you show a before/after? Alternatively, if it's as little work for you, a PR showing the same would be fine.

@CableZa
Copy link
Author

CableZa commented Jun 18, 2020

This only really matters when one needs to record numerous fakes/dependencies, but the utility class helps a lot with some common test setup code:

  1. this class would wrap the recorders in a idisposable, allowing cleaner test code without the need to explicitly call dispose to get the recordings to save.
  2. it helps create the fakes.

I don't have the before, but here is the after:

using (var testDoubleUtilities = new TestDoubleUtilities(recordingsBasePath, testClassName,  testName))
{
    var fakeDependency =  testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency);
    var sut = CreateSUT(fakeDependency);

    //ACT
    var result = await sut.ExecuteSomeMethodAsync();

    //ASSERT
    if (result is OkNegotiatedContentResult<Response>)
    {
          var response = ((OkNegotiatedContentResult<ConcreteResponseClass>)result).Content;
          Assert.IsTrue(response.SomeBoolParam);
          Assert.AreEqual(500m, response.SomeOtherParam);
    }
}

Without this, the boiler plate code is needed to create the folder structure for recording test doubles:

var basePath = Path.Combine(testRecordingsBasePath, testClassName, testName);
if (!Directory.Exists(basePath))
{
     Directory.CreateDirectory(basePath);
}

this.BasePath = basePath;

One would need to create the repo and fake for each dependency that needs faking, and remember to dispose it (below its being added to a list for later disposal)

var typeName = typeof(T).Name;

var recordedFilePath = Path.Combine(this.BasePath, typeName + ".json");

var fakeRepo = new JsonFileRecordedCallRepository(recordedFilePath);
var selfInitializingFake = SelfInitializingFake<T>.For(createRealService, fakeRepo);
this.fakesToDispose.Add(selfInitializingFake.Dispose);
return selfInitializingFake;

@CableZa
Copy link
Author

CableZa commented Jun 18, 2020

The interface for TestDoubleUtilities is

public TestDoubleUtilities(string testRecordingsBasePath, string testClassName, string testName){}

public SelfInitializingFake<T> CreateSelfInitializingTestDouble<T>(Func<T> createRealService) where T : class{}

@blairconrad
Copy link
Owner

I see, thanks. That's interesting.

I've never worried about the boilerplate for creating a directory for the files to live in. I just make the directory by hand once and after that, once I commit a file, the directory's known to be there forever. Are you doing something different so you have uncertainty around the directory's presence?

I can see how having multiple services be disposed might be handy, but am not really sure how the experience is that much better than just using a using for each one. Maybe I'm missing something?

And if I understand things right, the TestDoubleUtilities will be building the path to the persistent store (and is therefore only useful for file-based repositories), and also deciding which kind of repository to use (hard-coded to JsonFileRecordedCallRepository in your example). Is that right?

@CableZa
Copy link
Author

CableZa commented Jun 23, 2020

Are you doing something different so you have uncertainty around the directory's presence?

Not at all, I'm just trying to make it easier to follow a convention for test recordings :-)

but am not really sure how the experience is that much better than just using a using for each one. Maybe I'm missing something?

My thinking was around trying to keep the test code cleaner/easier to follow especially in cases where 4-5 classes/dependencies are being recorded in one test.
Otherwise you'd need 5 nested using statements to record and dispose all of the recorders. Unless my usage of the test double library is completely off :-|

And if I understand things right, the TestDoubleUtilities will be building the path to the persistent store (and is therefore only useful for file-based repositories), and also deciding which kind of repository to use (hard-coded to JsonFileRecordedCallRepository in your example). Is that right?

Correct, although I suppose it could take the recorder type in as a parameter somehow for flexibility?

file-based repositories

Interesting point - what other basis for repositories do you see possible for this? Databases? Web?

@blairconrad
Copy link
Owner

Are you doing something different so you have uncertainty around the directory's presence?

Not at all, I'm just trying to make it easier to follow a convention for test recordings :-)

Makes sense. I'm not sure it's up to the library to pick a convention, especially when it dictates serializer type and path structure. Although I guess why not? There's nothing inherently wrong with providing a convention. People don't have to use it. But I'm just not seeing that it makes things that much easier to use, so far. To me, the complexity is nearly the same, with more code to maintain in the library.

but am not really sure how the experience is that much better than just using a using for each one. Maybe I'm missing something?

My thinking was around trying to keep the test code cleaner/easier to follow especially in cases where 4-5 classes/dependencies are being recorded in one test.
Otherwise you'd need 5 nested using statements to record and dispose all of the recorders. Unless my usage of the test double library is completely off :-|

Yeah, it would typically be 5 usings, although they don't have to visually nest. I converted your example as best I could to a 5-service situation:

using (var testDoubleUtilities = new TestDoubleUtilities(recordingsBasePath, testClassName,  testName))
{
    var fakeDependency1 = testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency1);
    var fakeDependency2 = testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency2);
    var fakeDependency3 = testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency3);
    var fakeDependency4 = testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency4);
    var fakeDependency5 = testDoubleUtilities.CreateSelfInitializingTestDouble(createRealDependency5);
    var sut = CreateSUT(fakeDependency1, fakeDependency2, fakeDependency3, fakeDependency4, fakeDependency5);}

and rewrote as original (I haven't run this, but I think it's about right)

using (var fakeDependency1 = new SelfInitializingFake<IService1>.For(createRealDependency1, new XmlFileRecordedCallRepository("base/testclass/testname/service1.xml")))
using (var fakeDependency2 = new SelfInitializingFake<IService2>.For(createRealDependency2, new XmlFileRecordedCallRepository("base/testclass/testname/service2.xml")))
using (var fakeDependency3 = new SelfInitializingFake<IService3>.For(createRealDependency3, new XmlFileRecordedCallRepository("base/testclass/testname/service3.xml")))
using (var fakeDependency4 = new SelfInitializingFake<IService4>.For(createRealDependency4, new XmlFileRecordedCallRepository("base/testclass/testname/service4.xml")))
using (var fakeDependency5 = new SelfInitializingFake<IService5>.For(createRealDependency5, new XmlFileRecordedCallRepository("base/testclass/testname/service5.xml")))
{
    var sut = CreateSUT(fakeDependency1, fakeDependency2, fakeDependency3, fakeDependency4, fakeDependency5);}

Aside from the fact that the 5 using lines in the second sample are longer, I'm not sure there's much benefit, and even that advantage could be lessened by having the test-writer write their own repo-creating function.

In the TestDoubleUtilities case, it's impossible to make 2 fakes of the same type, no? Their recorded files would conflict. Or I guess some disambiguation could be built-in: "typeName", "typeName001", etc.…

And if I understand things right, the TestDoubleUtilities will be building the path to the persistent store (and is therefore only useful for file-based repositories), and also deciding which kind of repository to use (hard-coded to JsonFileRecordedCallRepository in your example). Is that right?

Correct, although I suppose it could take the recorder type in as a parameter somehow for flexibility?

Adding a recorder type is appealing. Of course, there'd need to be a consistent interface for the creation method in that case. If we assume file-based, then a single string in the constructor would work.
For that matter, it might work for other repository types as well.

Interesting point - what other [non-file] basis for repositories do you see possible for this? Databases? Web?

My guess, if any non-file repo is ever implemented, would be database. But I suppose some sort of web-based storage is possible.
For internal tests, I'm using an in-memory repo, but that's a pretty niche cases, as an ephemeral repo sort of flies in the face of the intended usage of the library!

@blairconrad
Copy link
Owner

@CableZa, I was thinking while mowing the lawn last night, and figured that 2 aspects of this proposal were low-enough effort (and maintenance) with immediate benefits to users, so I cannibalized them. See #87 and #88.

@CableZa
Copy link
Author

CableZa commented Jun 26, 2020

Awesome thanks very much @blairconrad!

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

No branches or pull requests

2 participants