Scrubbers run on the final string before doing the verification action.
Multiple scrubbers can be defined at multiple levels.
Scrubbing is performed by two mechanisms:
- The scrub engine: a span based engine that performs all built-in scrubbing (
ScrubLinesContaining,ScrubInlineGuids,ScrubMachineName, etc). - Legacy scrubbers:
AddScrubber(Action<StringBuilder>)overloads. These run after the engine, and only when at least one is registered.
Each engine operation is available as a Scrub* method at any level:
ScrubReplace(find, replacement): replace every occurrence of a string. Supports an ordinalStringComparison(OrdinalorOrdinalIgnoreCase) and an optional word boundary requirement. A multi-pair overload replaces the longest matching find at any position.ScrubWindow(minLength, maxLength, matcher): slide a window over the text; the matcher returns a replacement or null. Used by the inline guid and date scrubbers.ScrubMatch(matcher, minLength, maxLength): custom search logic. The matcher locates the next match within a segment.ScrubLinesContaining(...),ScrubLines(...),ScrubLinesWithReplace(...),ScrubEmptyLines(): line scoped scrubbing.
verifySettings.ScrubReplace("abc", "xyz");
verifySettings.ScrubWindow(
minLength: 3,
maxLength: 10,
matcher: (window, _, _) =>
{
if (window.StartsWith("id-"))
{
return "{Id}";
}
return null;
});ScrubLines and ScrubLinesWithReplace also accept span based delegates (LineMatch / LineReplace) that avoid allocating a string per line. Use an explicitly typed lambda parameter to select them, e.g. ScrubLines((ReadOnlySpan<char> line) => ...); untyped lambdas bind the string overloads.
Engine semantics:
- Quarantine: text produced by a replacement is never re-examined by other engine scrubbers. Legacy scrubbers run afterwards and can still modify it.
- Ordering is engine determined, not registration determined: line removals run first, then line transforms (registration order), then inline scrubbers (unknown max length first, then longest max length first, ties broken by level then registration order). Directory replacements always run last, so scrubbers always see raw paths.
- Length skip: text shorter than a scrubber's minimum match length is never scanned by that scrubber.
- Single line rule: a match may never contain a line break.
- Text is newline normalized (
\r\nand\rbecome\n) before scrubbers run.
ScrubberLocation on the built-in scrub methods is obsolete and ignored; it still applies to legacy AddScrubber overloads (default First executes in reverse registration order, Last in registration order).
Instead of being executed by the engine, AddScrubber(Action<StringBuilder>) mutates the full text directly. Overloads also accept a Counter, and the context dictionary:
verifySettings.AddScrubber(_ => _.Remove(0, 100));Since these run after every engine scrubber, they can also modify text that the engine has already replaced.
Scrubbers can be added to an instance of VerifySettings or globally on VerifierSettings.
- The current solution directory will be replaced with
{SolutionDirectory}. To disable useVerifierSettings.DontScrubSolutionDirectory()in a module initializer. See solution Discovery - The current project directory will be replaced with
{ProjectDirectory}. To disable useVerifierSettings.DontScrubProjectDirectory()in a module initializer. - On Windows, the current user profile will be replaced with
{UserProfile}. To disable useVerifierSettings.DontScrubUserProfile()in a module initializer. - The
AppDomain.CurrentDomain.BaseDirectorywill be replaced with{CurrentDirectory}. - The
Assembly.CodeBasewill be replaced with{CurrentDirectory}. - The
Path.GetTempPath()will be replaced with{TempPath}.
The solution and project directory replacement functionality is achieved by adding attributes to the target assembly at compile time. For any project that references Verify, the following attributes will be added:
[assembly: AssemblyMetadata("Verify.ProjectDirectory", "C:\Code\TheSolution\Project\")]
[assembly: AssemblyMetadata("Verify.SolutionDirectory", "C:\Code\TheSolution\")]
This information can be useful to consumers when writing tests, so it is exposed via AttributeReader:
- Project directory for an assembly:
AttributeReader.GetProjectDirectory(assembly) - Project directory for the current executing assembly:
AttributeReader.GetProjectDirectory() - Solution directory for an assembly:
AttributeReader.GetSolutionDirectory(assembly) - Solution directory for the current executing assembly:
AttributeReader.GetSolutionDirectory()
Allows lines to be selectively removed using a Func.
For example remove lines containing text:
verifySettings.ScrubLines(line => line.Contains("text"));Remove all lines containing any of the defined strings.
For example remove lines containing text1 or text2
verifySettings.ScrubLinesContaining("text1", "text2");Case insensitive by default (StringComparison.OrdinalIgnoreCase).
StringComparison can be overridden:
verifySettings.ScrubLinesContaining(StringComparison.Ordinal, "text1", "text2");Allows lines to be selectively replaced using a Func.
For example converts lines to upper case:
verifySettings.ScrubLinesWithReplace(line => line.ToUpper());Replaces Environment.MachineName with TheMachineName.
verifySettings.ScrubMachineName();Replaces Environment.UserName with TheUserName.
verifySettings.ScrubUserName();Adds a scrubber with full control over the text via a Func
Given the following target
static object BuildTarget() =>
new Target(
"C:/Code/TheSolution",
"C:/Code/TheSolution/TheProject",
new Date(2020, 1, 1),
new DateTime(2020, 1, 1),
new DateTimeOffset(2020, 1, 1, 1, 1, 1, TimeSpan.FromHours(10)),
new Guid("ae8529a6-30a0-46e2-b7d6-9fcb7b23463c"));When scrubbers are disabled the result will be:
{
TheSolutionDir: C:/Code/TheSolution,
TheProjectDir: C:/Code/TheSolution/TheProject,
Date: 2020-01-01,
DateTime: 2020-01-01,
DateTimeOffset: 2020-01-01 01:01:01 +10,
Guid: ae8529a6-30a0-46e2-b7d6-9fcb7b23463c
}[Fact]
public Task Instance()
{
var settings = new VerifySettings();
settings.DisableScrubbers();
return Verify(BuildTarget(), settings);
}[Fact]
public Task Fluent() =>
Verify(BuildTarget())
.DisableScrubbers();[TestFixture]
public class ScrubbersSample
{
[Test]
public Task Lines()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
});
settings.ScrubLines(removeLine: _ => _.Contains('J'));
settings.ScrubLinesContaining("b", "D");
settings.ScrubLinesContaining(StringComparison.Ordinal, "H");
return Verify(
settings: settings,
target: """
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""");
}
[Test]
public Task LinesFluent() =>
Verify("""
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""")
.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
})
.ScrubLines(removeLine: _ => _.Contains('J'))
.ScrubLinesContaining("b", "D")
.ScrubLinesContaining(StringComparison.Ordinal, "H");
[Test]
public Task RemoveOrReplace() =>
Verify("""
LineA
LineB
LineC
""")
.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineB"))
{
return null;
}
return line.ToLower();
});
[Test]
public Task EmptyLines() =>
Verify("""
LineA
LineC
""")
.ScrubEmptyLines();
}public class ScrubbersSample
{
[Fact]
public Task Lines()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineE"))
{
return "NoMoreLineE";
}
return line;
});
settings.ScrubLines(removeLine: _ => _.Contains('J'));
settings.ScrubLinesContaining("b", "D");
settings.ScrubLinesContaining(StringComparison.Ordinal, "H");
return Verify(
settings: settings,
target: """
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""");
}
[Fact]
public Task EmptyLine()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: _ => "");
return Verify(
settings: settings,
target: "");
}
[Fact]
public Task LinesFluent() =>
Verify("""
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""")
.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
})
.ScrubLines(removeLine: _ => _.Contains('J'))
.ScrubLinesContaining("b", "D")
.ScrubLinesContaining(StringComparison.Ordinal, "H");
[Fact]
public Task RemoveOrReplace() =>
Verify("""
LineA
LineB
LineC
""")
.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineB"))
{
return null;
}
return line.ToLower();
});
[Fact]
public Task EmptyLines() =>
Verify("""
LineA
LineC
""")
.ScrubEmptyLines();
}public class ScrubbersSample
{
public Task Lines()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
});
settings.ScrubLines(removeLine: _ => _.Contains('J'));
settings.ScrubLinesContaining("b", "D");
settings.ScrubLinesContaining(StringComparison.Ordinal, "H");
return Verify(
settings: settings,
target: """
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""");
}
public Task LinesFluent() =>
Verify("""
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""")
.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
})
.ScrubLines(removeLine: _ => _.Contains('J'))
.ScrubLinesContaining("b", "D")
.ScrubLinesContaining(StringComparison.Ordinal, "H");
public Task RemoveOrReplace() =>
Verify("""
LineA
LineB
LineC
""")
.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineB"))
{
return null;
}
return line.ToLower();
});
public Task EmptyLines() =>
Verify("""
LineA
LineC
""")
.ScrubEmptyLines();
}[TestClass]
public partial class ScrubbersSample
{
[TestMethod]
public Task Lines()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
});
settings.ScrubLines(removeLine: _ => _.Contains('J'));
settings.ScrubLinesContaining("b", "D");
settings.ScrubLinesContaining(StringComparison.Ordinal, "H");
return Verify(
settings: settings,
target: """
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""");
}
[TestMethod]
public Task LinesFluent() =>
Verify("""
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""")
.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
})
.ScrubLines(removeLine: _ => _.Contains('J'))
.ScrubLinesContaining("b", "D")
.ScrubLinesContaining(StringComparison.Ordinal, "H");
[TestMethod]
public Task RemoveOrReplace() =>
Verify("""
LineA
LineB
LineC
""")
.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineB"))
{
return null;
}
return line.ToLower();
});
[TestMethod]
public Task EmptyLines() =>
Verify("""
LineA
LineC
""")
.ScrubEmptyLines();
}public class ScrubbersSample
{
[Test]
public Task Lines()
{
var settings = new VerifySettings();
settings.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
});
settings.ScrubLines(removeLine: _ => _.Contains('J'));
settings.ScrubLinesContaining("b", "D");
settings.ScrubLinesContaining(StringComparison.Ordinal, "H");
return Verify(
settings: settings,
target: """
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""");
}
[Test]
public Task LinesFluent() =>
Verify("""
LineA
LineB
LineC
LineD
LineE
LineH
LineI
LineJ
""")
.ScrubLinesWithReplace(
replaceLine: _ =>
{
if (_.Contains("LineE"))
{
return "NoMoreLineE";
}
return _;
})
.ScrubLines(removeLine: _ => _.Contains('J'))
.ScrubLinesContaining("b", "D")
.ScrubLinesContaining(StringComparison.Ordinal, "H");
[Test]
public Task RemoveOrReplace() =>
Verify("""
LineA
LineB
LineC
""")
.ScrubLinesWithReplace(
replaceLine: line =>
{
if (line.Contains("LineB"))
{
return null;
}
return line.ToLower();
});
[Test]
public Task EmptyLines() =>
Verify("""
LineA
LineC
""")
.ScrubEmptyLines();
}LineA
LineC
NoMoreLineE
LineIScrubbers can be scoped to verified files with a matching extension by passing the extension as the first argument. The extension is specified without a leading dot:
verifySettings.ScrubReplace("json", "abc", "xyz");A scrubber registered this way runs only for verified files with that extension, while a scrubber registered without an extension runs for all of them. Extension scoping is available at every level, and for the legacy AddScrubber(Action<StringBuilder>) overloads.
Scrubbers can be defined at three levels:
- Method: Will run the verification in the current test method.
- Class: As a class level 'VerifySettings' field then re-used at the method level.
- Global: Will run for test methods on all tests.
[TestFixture]
public class ScrubberLevelsSample
{
VerifySettings classLevelSettings;
public ScrubberLevelsSample()
{
classLevelSettings = new();
classLevelSettings.AddScrubber(_ => _.Replace("Three", "C"));
}
[Test]
public Task Simple()
{
var settings = new VerifySettings(classLevelSettings);
settings.AddScrubber(_ => _.Replace("Two", "B"));
return Verify("One Two Three", settings);
}
[Test]
public Task SimpleFluent() =>
Verify("One Two Three", classLevelSettings)
.AddScrubber(_ => _.Replace("Two", "B"));
[ModuleInitializer]
public static void Setup() =>
VerifierSettings.AddScrubber(_ => _.Replace("One", "A"));
}public class ScrubberLevelsSample
{
VerifySettings classLevelSettings;
public ScrubberLevelsSample()
{
classLevelSettings = new();
classLevelSettings.AddScrubber(_ => _.Replace("Three", "C"));
}
[Fact]
public Task Usage()
{
var settings = new VerifySettings(classLevelSettings);
settings.AddScrubber(_ => _.Replace("Two", "B"));
return Verify("One Two Three", settings);
}
[Fact]
public Task UsageFluent() =>
Verify("One Two Three", classLevelSettings)
.AddScrubber(_ => _.Replace("Two", "B"));
[ModuleInitializer]
public static void Initialize() =>
VerifierSettings.AddScrubber(_ => _.Replace("One", "A"));
}public class ScrubberLevelsSample
{
VerifySettings classLevelSettings;
public ScrubberLevelsSample()
{
classLevelSettings = new();
classLevelSettings.AddScrubber(_ => _.Replace("Three", "C"));
}
public Task Simple()
{
var settings = new VerifySettings(classLevelSettings);
settings.AddScrubber(_ => _.Replace("Two", "B"));
return Verify("One Two Three", settings);
}
public Task SimpleFluent() =>
Verify("One Two Three", classLevelSettings)
.AddScrubber(_ => _.Replace("Two", "B"));
[ModuleInitializer]
public static void Setup() =>
VerifierSettings.AddScrubber(_ => _.Replace("One", "A"));
}[TestClass]
public partial class ScrubberLevelsSample
{
VerifySettings classLevelSettings;
public ScrubberLevelsSample()
{
classLevelSettings = new();
classLevelSettings.AddScrubber(_ => _.Replace("Three", "C"));
}
[TestMethod]
public Task Simple()
{
var settings = new VerifySettings(classLevelSettings);
settings.AddScrubber(_ => _.Replace("Two", "B"));
return Verify("One Two Three", settings);
}
[TestMethod]
public Task SimpleFluent() =>
Verify("One Two Three", classLevelSettings)
.AddScrubber(_ => _.Replace("Two", "B"));
[AssemblyInitialize]
public static void Setup(TestContext testContext) =>
VerifierSettings.AddScrubber(_ => _.Replace("One", "A"));
}public class ScrubberLevelsSample
{
VerifySettings classLevelSettings;
public ScrubberLevelsSample()
{
classLevelSettings = new();
classLevelSettings.AddScrubber(_ => _.Replace("Three", "C"));
}
[Test]
public Task Simple()
{
var settings = new VerifySettings(classLevelSettings);
settings.AddScrubber(_ => _.Replace("Two", "B"));
return Verify("One Two Three", settings);
}
[Test]
public Task SimpleFluent() =>
Verify("One Two Three", classLevelSettings)
.AddScrubber(_ => _.Replace("Two", "B"));
[ModuleInitializer]
public static void Setup() =>
VerifierSettings.AddScrubber(_ => _.Replace("One", "A"));
}A B C