Easy is a tool that makes test-driven development easier. At the beginning of doing TDD, I found it awkward that I needed to write more code for tests than for actual production code. It was also inconvenient to spend more time writing test code than production code. To address this, I created a couple of code generation scripts that improved performance, but they were not user-friendly. That's how easyTdd was born. The tool is still in its early stages and is being developed iteratively. It works with MsTest, NUnit, Xunit test frameworks and can generate tests, mocks, builders, and test cases.
Release notes
Version 0.5.1
Bug fix
- Fixed indremental fluentMock template to include a namespace for a generic IEnumerable.
Version 0.5.0.4
Bug fix
- Resolved an issue in the builder where code was being generated for properties that do not have a public setter.
Version 0.5.0.3
Bug fix
- Resolved an issue in builder and test generation where a derived class overriding a member would result in code being generated for both the base class member and the overridden member of the derived class.
Version 0.5.0.2
- Updated migration to version 0.5.0: The System.Collections.Generic namespace was not included in the tooling namespace list for the builder.
- Fixed test case generation: Quick Action menu items for test case generation were not displayed correctly.
- Adjusted the order of FluentMock Quick Action menu items.
Version 0.5.0.1
Updates and fixes:
- Updated migration to 0.5.0 version.
System.Linq
was not added to the tooling namespase list for the builder.
- Updated how constructor parameters and setter properties are matched for a builder model.
Version 0.5.0.0
New Feature
- Introduced incremental code generator for fluentMocks. The FluentMock creates a wrapper class around the Moq.Mock class, with setup and verify methods automatically generated for each method and property of the target class. This approach ensures a smoother process for creating, setting up, and verifying mocks. The implementation leverages the fluent interface design pattern, which improves code readability and intuitiveness by enabling chained method calls that resemble natural language.
Updates and fixes:
- Fixed templates to take if nulable is enabled for a project into account
- Changed names of generated file to not collide with indremental generated files
- Added links to a menu to all generated files.
- Updated mock templated to support more scenarious.
- Fixed builder template to support empty class.
- Updated test, builder and mock templates to take into account members of a base class.
- Rebranded a mock to a FluentMock
- Added
Build(int count)
to the builder template to build multiple objects.
- Fixed Test Cases in External Class to not produce a namespace if one already exists.
- Fixed Test Cases and Test Cases in External Class generation to not require test attributes on the test. Also update attribute templetes to include all necessary attributes.
- Added support for test generation for struct and record types.
Version 0.4.0.5
Update:
- Updated to install version 0.4.0.5 of EasyTdd.Generators, which includes a bug fix where generation failed when the incremental builder was generated for two classes in different namespaces but with the same name.
Version 0.4.0.4
Bug fixes:
- Fixed a crash in EasyTdd that occurred on certain models due to a Stack Overflow Exception caused by infinite recursion.
Version 0.4.0.3
Bug fixes:
- Updated to install version 0.4.0.3 of EasyTdd.Generators.
- Fixed a bug in test generation that caused a crash when an array was used as a dependency for a target class.
Version 0.4.0.2
Bug fixes:
- Fixed to install 0.4.0 version of EasyTdd.Generators.
Version 0.4.0.1
Bug fixes:
- Fixed the issue where the tool providers were loaded before the migration of settings was completed in the background.
Version 0.4.0
New features
- The Incremental Builder, powered by the IIncrementalGenerator within the EasyTdd.Generators NuGet package, creates builders using settings and templates from the .easyTdd folder. This builder generator efficiently manages property setters, constructor parameters, and their combinations. It also supports generic parameters, enhancing its flexibility and utility.
Bug fixes:
- Number of know bugs were fixed.
Version 0.3.8.4
Bug fixes:
- Resolved the issue with adding project reference when the source and target projects are identical.
Version 0.3.8.3
Bug fixes:
- Adjusted the test class suffix to be derived from the settings.json file.
New feature
- Automatically include a source project reference in the target project upon generation of the target class.
Version 0.3.8.2
Bug fixes:
- The .easyTdd configuration folder was only created when a solution was opened. Fixed to create the .easyTdd configuration folder on solution creation as well.
- Removed obsolete EasyTdd options.
Version 0.3.8.1
Bug fix:
- Sometimes the tool is loaded faster than solutions. It caused issues if one of the features were called before solutions is loaded.
Version 0.3.8
Template Management:
- Default templates have been relocated to the "DefaultTemplates" folder.
- Users can now override default templates, making customization easier.
- To apply changes, simply copy and paste the template, and remember to update the template name in the
settings.json
file.
Settings Migration:
- Introducing support for automatic migration of user settings between versions.
- All user-modified settings will seamlessly transition to the next version.
Code Block Indentation Fix:
- Addressed and resolved issues related to how indentations are set for added code blocks.
Configuration
After installing the plugin, once the solution is opened for the first time, the ".easyTdd" folder is generated in the solution folder. It contains files for templates and a "settings.json" file for configuration. Scriban is used for template parsing.
ReSharper
EasyTdd features are implemented using the Visual Studio Quick Action menu. ReSharper might override this menu and hide it. To enable it, go to ReSharper's options and uncheck "Hide Visual Studio Quick Action icons in the left editor margin":
Main features
Generating mock class
It generates a class that inherits from the Moq.Mock class and has a fluent, chainable structure, which is easier to use in easyTdd scenarios. You can customize it by modifying the "mock.tpl" file in the ".easyTdd" folder. To generate a mock, right-click on an interface or abstract class and select "Generate Mock":
A mock is generated, and the file is opened:
If mocks have already been generated for interfaces or abstract classes, you will see options for "Open [FooMock.cs]" and "Regenerate [FooMock.cs]":
Generating builders
The builder pattern is used for concrete class usage in tests. You can generate a builder by right-clicking on a concrete class and selecting "Generate Builder":
A builder is generated, and the file is opened:
For classes that already have a builder generated, you will see options for "Open [FooBuilder.cs]" and "Regenerate [FooBuilder.cs]":
You can customize it by modifying the "builder.tpl" file in the ".easyTdd" folder.
Generating tests
The "Generate Test" action creates a test file, generates dependencies in the form of test doubles, creates the subject under test, and generates methods for testing invalid input cases and for dependency calls:
The output is opened:
Feel free to remove anything that is not needed; it is easier to remove than to type everything by hand. For classes that already have a test generated, you will see options for "Open [FooTest.cs]" and "Regenerate [FooTest.cs]":
You can customize it by modifying the "[test framework].test.tpl" file in the ".easyTdd" folder.
Generating test cases
The "Generate Test Cases" action generates a test case method for parameterized tests:
An attribute for the test is added, and the source method is generated:
You can customize it by modifying the "[test framework].test-cases.attribute.tpl" and "[test framework].test-cases.source-method" files in the ".easyTdd" folder.
Generating test cases in an external file
"Generated Test Cases in External File" generates a test case class for parameterized tests:
An attribute for the test is added, and the source file is generated:
You can customize it by modifying the "[test framework].test-cases-external.attribute.tpl" and "[test framework].test-cases-external.source-class.tpl" files in the ".easyTdd" folder.
Generate incremental builder
The incremental builder, powered by the IIncrementalGenerator, automatically updates without needing regeneration when target class changes occur. This builder generator efficiently manages property setters, constructor parameters, and their combinations. It also supports generic parameters, significantly enhancing its flexibility and utility.
Target class SampleClass.cs
public class SampleClass<T>
where T : class
{
public SampleClass(T value, int id)
{
Value = value;
Id = id;
}
public T Value { get; }
public int Id { get; }
public string Name { get; set; }
public string Description { get; set; }
}
Builder class SampleClassBuilder.cs
[BuilderFor(typeof(SampleClass<>))]
public partial class SampleClassBuilder<T>
{
public SampleClassBuilder<T> Default()
{
return new SampleClassBuilder<T>(
default,
1,
"Jovaras",
"Augalas"
);
}
}
Generated class SampleClassBuilder.g.cs
public partial class SampleClassBuilder<T>
where T : class
{
private Func<T> _value;
private Func<int> _id;
private Func<string> _name;
private Func<string> _description;
public static implicit operator EasyTdd.Generators.ForTestingByNugetPackageReference.SampleClass<T>(SampleClassBuilder<T> builder) => builder.Build();
public SampleClassBuilder(
Func<T> value,
Func<int> id,
Func<string> name,
Func<string> description)
{
_value = value;
_id = id;
_name = name;
_description = description;
}
public SampleClassBuilder(
T value,
int id,
string name,
string description)
{
_value = () => value;
_id = () => id;
_name = () => name;
_description = () => description;
}
public EasyTdd.Generators.ForTestingByNugetPackageReference.SampleClass<T> Build()
{
return new EasyTdd.Generators.ForTestingByNugetPackageReference.SampleClass<T>(
_value(),
_id())
{
Name = _name(),
Description = _description()
};
}
public SampleClassBuilder<T> WithValue(Func<T> value)
{
_value = value;
return this;
}
public SampleClassBuilder<T> WithValue(T value)
{
return WithValue(() => value);
}
public SampleClassBuilder<T> WithId(Func<int> value)
{
_id = value;
return this;
}
public SampleClassBuilder<T> WithId(int value)
{
return WithId(() => value);
}
public SampleClassBuilder<T> WithName(Func<string> value)
{
_name = value;
return this;
}
public SampleClassBuilder<T> WithName(string value)
{
return WithName(() => value);
}
public SampleClassBuilder<T> WithDescription(Func<string> value)
{
_description = value;
return this;
}
public SampleClassBuilder<T> WithDescription(string value)
{
return WithDescription(() => value);
}
}
Generate incremental fluentMock
The incremental fluentMock, powered by the IIncrementalGenerator, automatically updates without needing regeneration when target class changes occur. The FluentMock creates a wrapper class around the Moq.Mock class, with setup and verify methods automatically generated for each method and property of the target class. This approach ensures a smoother process for creating, setting up, and verifying mocks. The implementation leverages the fluent interface design pattern, which improves code readability and intuitiveness by enabling chained method calls that resemble natural language.
Target interface ISomeInterface.cs
public interface ISomeInterface
{
SomeResultType Resolve(int id, string value, SomeOtherResultType someOtherResultType);
}
FluentMock class SomeInterfaceMock.cs
[EasyTdd.Generators.FluentMockFor(typeof(ISomeInterface))]
public partial class SomeInterfaceMock
{
public static SomeInterfaceMock Create()
{
return new SomeInterfaceMock();
}
}
Generated FluentMock class SomeInterfaceMock.g.cs
// <auto-generated>
// This code was generated by EasyTdd.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated. Use a partial class for additional behavior.
// </auto-generated>
#nullable enable
using Moq;
using System;
namespace EasyTdd.Generators.ForTestingByProjectReference.ForDebug
{
public partial class SomeInterfaceMock : Mock<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.ISomeInterface>
{
public SomeInterfaceMock() : base(MockBehavior.Strict)
{ }
public SomeInterfaceMock SetupResolve(
Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeResultType> result,
Func<int, bool>? isId = null,
Func<string, bool>? isValue = null,
Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType, bool>? isSomeOtherResultType = null,
bool verifiable = true)
{
var setup = Setup(
x => x.Resolve(
It.Is<int>(fromMock => isId == null || isId(fromMock)),
It.Is<string>(fromMock => isValue == null || isValue(fromMock)),
It.Is<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType>(fromMock => isSomeOtherResultType == null || isSomeOtherResultType(fromMock))
)
)
.Returns(result);
if (verifiable)
{
setup.Verifiable();
}
return this;
}
public SomeInterfaceMock SetupResolve(
Func<int, string, EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType, EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeResultType> result,
Func<int, bool>? isId = null,
Func<string, bool>? isValue = null,
Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType, bool>? isSomeOtherResultType = null,
bool verifiable = true)
{
var setup = Setup(
x => x.Resolve(
It.Is<int>(fromMock => isId == null || isId(fromMock)),
It.Is<string>(fromMock => isValue == null || isValue(fromMock)),
It.Is<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType>(fromMock => isSomeOtherResultType == null || isSomeOtherResultType(fromMock))
)
)
.Returns(result);
if (verifiable)
{
setup.Verifiable();
}
return this;
}
public SomeInterfaceMock SetupResolveSequence(
IEnumerable<Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeResultType>> results,
Func<int, bool>? isId = null,
Func<string, bool>? isValue = null,
Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType, bool>? isSomeOtherResultType = null)
{
var sequenceSetup =
SetupSequence(
x => x.Resolve(
It.Is<int>(fromMock => isId == null || isId(fromMock)),
It.Is<string>(fromMock => isValue == null || isValue(fromMock)),
It.Is<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType>(fromMock => isSomeOtherResultType == null || isSomeOtherResultType(fromMock))
)
);
foreach (var result in results)
{
sequenceSetup.Returns(result);
}
return this;
}
public SomeInterfaceMock VerifyResolve(
Func<int, bool>? isId = null,
Func<string, bool>? isValue = null,
Func<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType, bool>? isSomeOtherResultType = null,
Func<Times>? times = null)
{
Verify(
x => x.Resolve(
It.Is<int>(fromMock => isId == null || isId(fromMock)),
It.Is<string>(fromMock => isValue == null || isValue(fromMock)),
It.Is<EasyTdd.Generators.ForTestingByProjectReference.ForDebug.SomeOtherResultType>(fromMock => isSomeOtherResultType == null || isSomeOtherResultType(fromMock))
),
times ?? Times.AtLeastOnce
);
return this;
}
}
}
FluentMock usage
public class UsageTest
{
[Test]
public void Test()
{
var expectedResult1 = new SomeResultType();
var expectedResult2 = new SomeResultType();
var mock = SomeInterfaceMock
.Create()
.SetupResolve(
() => expectedResult1,
isId: x => x == 1
)
.SetupResolve(
() => expectedResult2,
isId: x => x == 2
);
mock
.Object
.Resolve(
1,
"",
new SomeOtherResultType()
)
.Should()
.Be(expectedResult1);
mock
.Object
.Resolve(
2,
"",
new SomeOtherResultType()
)
.Should()
.Be(expectedResult2);
}
}
More about the EasyTdd
More information on how to use and configure EasyTdd can be found at easytdd.dev and at www.youtube.com/@EasyTdd_dev