ConventionMapper.Testing 1.9.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package ConventionMapper.Testing --version 1.9.1
                    
NuGet\Install-Package ConventionMapper.Testing -Version 1.9.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ConventionMapper.Testing" Version="1.9.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ConventionMapper.Testing" Version="1.9.1" />
                    
Directory.Packages.props
<PackageReference Include="ConventionMapper.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ConventionMapper.Testing --version 1.9.1
                    
#r "nuget: ConventionMapper.Testing, 1.9.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ConventionMapper.Testing@1.9.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ConventionMapper.Testing&version=1.9.1
                    
Install as a Cake Addin
#tool nuget:?package=ConventionMapper.Testing&version=1.9.1
                    
Install as a Cake Tool

ConventionMapper.Testing

Test helpers for mappings written with ConventionMapper or ConventionMapper.SourceGenerator. No dependency on a test framework: failures throw MapperAssertException, which xUnit, NUnit and MSTest all report.

dotnet add package ConventionMapper.Testing

Which members does a map write?

MapperAssert.WritesExactly fills a source object (distinct values, nested objects, one-element collections), runs the map and compares the destination members that got a value with the list you expect:

MapperAssert.WritesExactly((LocationData s) => s.ToImportData("GL1", 10m),
    "LocationNumber", "Street1", "City", "GlClassCode", "Exposure");

With the source generator, each mapper class has WriteSets: the members each generated method writes. Compare the real behavior with it, without listing members by hand:

MapperAssert.WritesExactly((LocationData s) => s.ToImportData(), Mappings.WriteSets["ToImportData(LocationData)"]);

Keys are "Name(ParameterTypes)" as in the data-flow report, plus "Name" for methods without overloads. To survive renames and signature changes, look the write set up by name and parameter types instead: Mappings.WriteSetOf(nameof(Mappings.ToImportData), typeof(LocationData)), or MapperAssert.WriteSetOf(typeof(Mappings), nameof(Mappings.ToImportData), typeof(LocationData)). Update methods use MapperAssert.UpdatesExactly((Dto s, Entity t) => s.Update(t), expected).

Do the values come from the right members?

WritesExactly checks which members are written, not where their values come from: a swapped pair (SubmitUserId filled from CompleteUser.Id, CompleteUserId from SubmitUser.Id) passes it. MapperAssert.WritesFrom checks each destination member against the source member path it should be copied from. Every sample value is distinct, so a value that comes from the wrong member is found, and the failure names the member it really came from:

MapperAssert.WritesFrom((Claim c) => c.ToDto(), ("SubmitUserId", "SubmitUser.Id"), ("CompleteUserId", "CompleteUser.Id"));
// CompleteUserId: expected the value of CompleteUser.Id (12), but it holds 9, the value of SubmitUser.Id

The expected pairs must come from you (or from a spec), not from the code under test. With the source generator you can pass the generated lineage instead, MapperAssert.LineageOf(typeof(Mappings), nameof(Mappings.ToDto), typeof(Claim)), but that records the mapping configuration, so a configuration mistake such as a [MapMember] that swaps two members is in the lineage too and passes. It only proves the generated code does what the configuration says. Pair it with the snapshot-tested data-flow report, where a reviewer sees the swap, or state the pairs that matter explicitly.

Nested objects mapped by another method are left to that method's own check. Built-in conversions (numbers, ToString, enum names) are followed; a conversion it can't follow is only reported when the value clearly came from elsewhere.

A baseline for every map

[Fact]
public void Every_map_matches_its_write_set() =>
    MapperAssert.AllMappersMatchWriteSets(typeof(Mappings));

Runs every generated create and update method with sample data (extra parameters filled too) and compares it with the generator's WriteSets and Lineage: it must not throw, must not write members outside its write set, must write the members it copies from a filled source, and each value must come from the member the generator recorded. A new map gets this coverage before anyone writes a dedicated test.

What it can't catch: it compares the generated code with the generator's own record of the configuration, so a mistake in the configuration itself passes. If [MapMember("CompleteUser.Id", "SubmitUserId")] swaps two members, the code and the record agree, and the check passes. It finds problems the configuration doesn't show: exceptions on real values (a parse, a [MapRequired] member, a conversion method), and generator bugs. To check the configuration is right, state the expected sources yourself with WritesFrom (above) or the expected members with WritesExactly.

When sample values aren't valid input (a string that is parsed, say), pass options: AllMappersMatchWriteSets(typeof(Mappings), new SampleDataOptions().For<Row, string>(r => r.Code, n => n.ToString())). Projections and collection merges are skipped.

Round trips

For a map and its reverse, back(to(x)) must equal x, deeply (nested objects member by member, collections item by item):

MapperAssert.RoundTrips<Person, PersonDto>(Mappings.ToDto, Mappings.ToPerson);

// 100 randomized inputs: nullable members sometimes null, empty, blank, very long and non-ASCII strings,
// zero and extreme numbers, empty and multi-item collections.
MapperAssert.RoundTrips<Person, PersonDto>(Mappings.ToDto, Mappings.ToPerson, runs: 100);

// Members the reverse map can't restore on purpose:
MapperAssert.RoundTrips<Person, PersonLite>(Mappings.ToLite, Mappings.FromLite, ignore: new[] { "Nickname" });

A failure lists the member paths that changed, or the exception, with the seed of that run. Replay it with options: new SampleDataOptions().Randomized(seed).

Snapshots of the generated code

A library upgrade or an attribute change can alter what the generator writes for a mapper. Snapshot one method's generated code in its own file, so the change fails a test that names that mapper:

[Fact]
public void ToDto_generated_code_is_reviewed() =>
    MapperAssert.GeneratedCodeMatches(typeof(Mappings), nameof(Mappings.ToDto), new[] { typeof(Order) });
    // generated-code/Mappings.ToDto(Order).cs, next to this test file

[Fact]
public void Generated_code_is_reviewed() =>
    MapperAssert.GeneratedCodeMatchesDirectory(typeof(Mappings));   // generated-code/Mappings/<method>.cs, one per method

The project that contains the mapper needs <ConventionMapperEmbedGeneratedCode>true</ConventionMapperEmbedGeneratedCode>. A missing file is created and the test fails once so it gets reviewed and committed; a change fails with a line diff; run the tests with CONVENTIONMAPPER_UPDATE_SNAPSHOTS=1 to accept changes. In directory mode, a file for a method that no longer exists fails as stale.

Is every mapping method tested?

[Fact]
public void Every_mapping_method_has_a_test() =>
    MapperAssert.AllMethodsCovered(typeof(Mappings), typeof(MappingTests).Assembly);

Fails with the list of generated mapping methods that no code in the test assembly calls (directly, as a method group or inside an expression tree).

A call alone doesn't check anything, so there is a stricter version: every create and update method must run inside a WritesExactly or UpdatesExactly check (projections must at least be called).

[Fact]
public void Every_mapping_method_is_verified() =>
    MapperAssert.AllMethodsVerified(typeof(Mappings), typeof(MappingTests).Assembly);

Is every mapping method used?

[Fact]
public void No_unused_maps() =>
    MapperAssert.AllMethodsUsed(typeof(Mappings), typeof(OrderService).Assembly);

Fails for mapping methods that no code in the given production assemblies uses: maps only tests call, or nothing does, are candidates for deletion. A method reached through another used method of the mapper (a nested object's map) counts as used.

Sample data

SampleData.Create<T>() builds the filled objects used above. Strings carry the member name ("Street1-7"), so a value that lands in the wrong member is easy to spot in a failure. Constructor parameters are filled, and a private parameterless constructor (common on EF Core entities) is used when there is no public one.

SampleDataOptions customizes it; every check that fills objects takes them:

var options = new SampleDataOptions()
    .For<Address, string>(a => a.ZipCode, n => (10000 + n).ToString())   // valid values for one member
    .For<Money>(() => new Money(10m, "EUR"))                            // every value of a type
    .ForMember("CountryCode", () => "GB")                               // a member of this name in any type
    .Enum(Status.Active, Status.Closed)                                 // only these enum values
    .Construct<IShape>(() => new Square());                             // interfaces, or types without a usable constructor
options.CollectionSize = 3;           // items per collection (default 1)
options.MaxDepth = 2;                 // nesting depth (default 4)
options.FillNonPublicSetters = true;  // also private and init-only setters

var address = SampleData.Create<Address>(options);

Builders

SampleData.Builder<T>() starts from a filled object and sets only what a test cares about, so setup stays short:

var referral = SampleData.Builder<Referral>()
    .With(r => r.Status, "A")
    .With(r => r.Address.City, "Leeds")      // nested; missing objects on the way are created
    .Without(r => r.ClosedOn)                // null / default
    .Build();

var referrals = SampleData.Builder<Referral>().With(r => r.Number, i => 1000 + i).BuildMany(3);   // distinct values
var blank = SampleData.Builder<Referral>().Blank().With(r => r.Id, 7).Build();                     // start empty

Private and init-only setters and get-only auto-properties can be set, so entities that guard their state can be built too. Configure(r => ...) runs any other setup, and builders take the same SampleDataOptions.

Randomized(seed) switches to random edge cases, repeatable with the seed. Random values no longer carry member names, so WritesFrom and AllMappersMatchWriteSets refuse it; it is meant for RoundTrips and your own tests.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ConventionMapper.Testing:

Package Downloads
ConventionMapper.Testing.EntityFrameworkCore

Database checks for mappings into EF Core entities: DatabaseAssert.SavesRoundTrip maps, saves and reads back an entity inside a rolled-back transaction and reports every value the database changed (database defaults that replace a copied false or 0, truncated text, rounded numbers), and DatabaseAssert.ModelMatchesDatabase compares the EF Core model's columns, nullability, maximum lengths and defaults with the real schema (SQL Server and SQLite).

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.10.0 73 10/27/2026
1.9.1 118 10/27/2026
1.9.0 74 10/27/2026
1.8.0 122 10/26/2026

## 1.9.1 (2026-10-27)

Fixes from upgrading a large codebase to 1.9.0.

### Source generator
- **CM0028 no longer reports columns that only have a database default.** 1.9.0 treated any `HasDefaultValueSql(...)` column like an identity or computed column, so scaffolded entities with defaults such as `('U')` or `((1))` got false warnings (build errors with warnings as errors). A default is used only when an insert leaves the column out, so copying a value into it is normal. A default now counts as database-generated only on a key (`newid()`, `NEXT VALUE FOR` a sequence).
- **CM0028 says "column" for generated columns that aren't keys.** A computed or `ValueGeneratedOnAdd` column was reported as "the database-generated key"; it is now "the database-generated column", and the report note is `generated column` instead of `generated key`. Re-record data-flow snapshots that contain such a column.

### ConventionMapper.Testing
- Documentation: `AllMappersMatchWriteSets`, and `WritesFrom` with the generated `LineageOf`, compare the generated code with the generator's own record of the mapping configuration. They catch runtime failures and generator bugs, not configuration mistakes: a `[MapMember]` that swaps two members passes both. 1.9.0's docs said otherwise. To check the configuration, state the expected sources with `WritesFrom((Claim c) => c.ToDto(), ("SubmitUserId", "SubmitUser.Id"), ...)`.

### Build
- The test project rolls forward to newer runtimes, so `build.sh` works on a machine with only the .NET 10 SDK.

Full history: CHANGELOG.md in this package, or https://github.com/katragvb/Conventionmapper/blob/main/CHANGELOG.md