JustDummies 1.0.0-preview.6
dotnet add package JustDummies --version 1.0.0-preview.6
NuGet\Install-Package JustDummies -Version 1.0.0-preview.6
<PackageReference Include="JustDummies" Version="1.0.0-preview.6" />
<PackageVersion Include="JustDummies" Version="1.0.0-preview.6" />
<PackageReference Include="JustDummies" />
paket add JustDummies --version 1.0.0-preview.6
#r "nuget: JustDummies, 1.0.0-preview.6"
#:package JustDummies@1.0.0-preview.6
#addin nuget:?package=JustDummies&version=1.0.0-preview.6&prerelease
#tool nuget:?package=JustDummies&version=1.0.0-preview.6&prerelease
JustDummies
A fluent DSL for generating arbitrary yet valid test values — dummies: values a test needs but never asserts on.
Website: justdummies.io
The idea
A test's Arrange is full of values the test does not check: an order reference, a
quantity, a label. A hand-picked literal reads as significant even when it is not.
JustDummies makes the incidental legible as incidental — and, when the value must cross
an invariant (a value object, a contract precondition), the constraints express that
invariant, never what the test asserts:
string code = Any.String()
.NonEmpty()
.WithMaxLength(50)
.StartingWith("ORD-")
.Generate();
Read it as: any string that satisfies these constraints. The exact value does not matter — and that is the point.
What's inside
- Fluent, typed generators implementing
IAny<T>, materialized through.Generate(), across the .NET simple types:String,Char, every integer width (SByte/Byte/Int16/UInt16/Int32/UInt32/Int64/UInt64),Double/Single/Decimal(finite values only — never NaN or infinities, and never accepted as arguments either: see NaN and the infinities below for the way to draw one deliberately),Boolean,Guid,Enum<T>(declared members only — a[Flags]enum widens to every combination withAllowingCombinations()),TimeSpan,DateTime(UTC) andDateTimeOffset. On modern targets (net8.0) the surface extends toDateOnly,TimeOnly,Int128,UInt128andHalf; the package also targetsnetstandard2.0and runs on .NET Framework 4.7.2+, .NET Core 2.0+ and .NET 5+ for the widest reach — with the .NET Framework 4.7.2 floor exercised in CI, not merely advertised. - Strings from a regex:
Any.StringMatching(pattern)generates arbitrary strings that match a regular expression — the dummy for a format-validated value object. Home-grown (zero dependencies) over the regular subset of the pattern language; a non-regular construct (a lookaround, a backreference) is refused with a clear error rather than a silently non-matching value. The pattern is the whole shape — express a length or a prefix inside it, since building a value in the intersection of two regular languages is not something the library does — but the exclusion pair is there:Any.StringMatching(@"^ORD-\d{8}$").DifferentFrom(existing)never yieldsexisting. - A default that certifies something: an unconstrained
Any.String()draws 0 to 1024 characters from the whole of ASCII, control characters included, andAny.Char()the same. That is deliberately inconvenient — a dummy the code under test had no say in is what makes a passing test mean anything, and a short, tame one proves nothing about a\ror a 300-character input. Narrow it with the invariants your code actually has. - A guard the floor does not cover:
Any.String().NotBlank()requires at least one character that is not whitespace — what a constructor guarding withstring.IsNullOrWhiteSpacedemands, whichNonEmpty()does not give you:"\n\r"is not empty. Interior whitespace stays legal, so"a b"is a value it admits. Whitespace here is the BCL's ownchar.IsWhiteSpace, wider than theWhitespacesfamily, so the two conflict naming each side where the filler must supply the non-blank character — an anchored literal already carrying one settles it instead. - Character families, and every one of them narrows:
Printable,NonPrintable,Alpha,Numeric,AlphaNumeric,Punctuation(POSIX[:punct:]),WhitespacesandHexadecimal(RFC 4648) each occupy one slot, so a second one conflicts naming both sides;WithoutAlphaandWithoutNumericsubtract instead and accumulate. OnAny.String()andAny.Char()alike, since the two carry the same families. Nothing named reaches past ASCII: a pool following the runtime's Unicode version would draw differently on two target frameworks. - A declared bound is the bound you get:
WithMaxLength(50)draws across 0 to 50 andWithLengthBetween(1000, 5000)across the whole range. Every size argument is refused above one million, maxima included. - Custom alphabets:
Any.String().WithChars("αβγδε")draws the string from an explicit character pool — the general form of the named sets, and the way to reach non-ASCII text (accents, Greek, Cyrillic, CJK) without aStringMatchingliteral. It stays within the Basic Multilingual Plane and rejects a surrogate: an emoji or other astral character is an atomic grapheme, not a character family, so draw those as whole strings withOneOf("😀", "🎉")instead. Anchored fragments are exempt: a pool is what the generator draws from, so a prefix, a suffix or a contained value you wrote is kept as written even when the pool could not produce it. - Strings from an explicit set:
Any.String().OneOf("EUR", "USD", "GBP")draws from a fixed, closed list — the dummy for a value whose domain is a short enumeration (a currency code, a well-known name). Composable like every other family'sOneOf: the other constraints narrow the set rather than shape a string, soAny.String().OneOf("abc", "de").WithLength(3)yields"abc"and a constraint no supplied value satisfies is a conflict naming both sides, whichever order the two were declared in. Duplicates collapse, and the draw is uniform and reproducible under a seed. - Any value from an explicit pool:
Any.OneOf(eur, usd, gbp)draws one value from a caller-supplied set of arbitrary values or domain objects, andAny.ElementOf(orders)does the same from a collection already held (a list, a LINQ result). This is the seed-aware answer to "any of these" — replacing a hand-rolledpool[new Random().Next(...)]that would ignore the seed and breakReproducibly. Uniform like the string set: duplicates collapse under the default comparer, the pool's distinct count gates distinct collections, and anullelement is refused — make the whole draw optional with.OrNull()instead. The element type is opaque to the library, so the pool is the whole shape of the specification; what it does offer is the exclusion pair, andAny.ElementOf(orders).DifferentFrom(theOneAlreadyUsed)is the idiom for drawing another element of a fixture. - URIs by family:
Any.Uri()yields an arbitrary yet validSystem.Uri— an absolute web (http/https), WebSocket (ws/wss), FTP or mailto URI, or a relative reference. Narrow it to a family and each returns a builder exposing only that family's valid components, so an impossible combination cannot even be written (Mailto()has noWithPort,WebSocket()noWithUserInfo):Any.Uri().Web().UsingHttps().WithHost("api.example.com"). Every part is drawn from ASCII-unreserved characters, so a value is valid by construction and reproducible across frameworks; internationalized (IDN) hosts and thefilescheme stay out of the default draw to keep that determinism. - Domain vocabulary where it belongs: dates constrain with
After/Before/Between, quantities withPositive/Between/NonZero, identities withNonEmpty/DifferentFrom— and deliberately no clock-relative constraints: a reproducible test pins its reference instants explicitly. - Values on a grid: a quantity that must be a whole number of some unit takes
MultipleOf—Any.Int32().Between(0, 100_000).MultipleOf(100)for an amount in whole euros held as cents — drawn on the grid so the declared range keeps its meaning, instead of anAs(x => x * 100)projection that silently distorts it.DecimaltakesWithScale(n), a value expressible inndecimal places (WithScale(2)for a currency amount) — a value lattice (a multiple of10⁻ⁿ), not a padded representation. The temporal generators takeWithGranularity(TimeSpan)— a round instant or duration (WithGranularity(TimeSpan.FromMinutes(15))) — so tick-precision values never surprise a serialization round-trip. Each is built in one draw, composes with the bounds and exclusions, and conflicts eagerly when the range holds no grid point. - Offset-aware
DateTimeOffset: unconstrained,Any.DateTimeOffset()carries offsetTimeSpan.Zero(UTC);WithOffset(TimeSpan)pins a whole-minute offset (±14:00) andWithOffsetBetween(min, max)draws a bounded one, so offset-sensitive code (local rendering, offset arithmetic, "same instant, different offset") is actually exercised. The instant is tightened first, so the value stays valid even at the edges of the range. Combined withOneOf(...), the declared offset selects which pooled values may be drawn — pooled values keep their own offset rather than being rewritten — and an offset none of them carries is a conflict, whichever of the two is declared first. - Values built to satisfy the constraints — a scalar is constructed directly,
never generated-then-filtered. The one exception is excluding values from a string or a
pattern (
Any.String().DifferentFrom(...)/Except(...),Any.StringMatching(p).DifferentFrom(...)): neither has an ordinal mapping to build the exclusion into, so it is met by a bounded redraw — the same escape a distinct collection uses to skip a duplicate, never an unbounded retry loop. An exclusion tight enough to leave nothing surfaces at generation as a seed-bearingAnyGenerationException, whose message reports the budget it spent rather than claiming no value remains — the search is bounded, so it never established that. An exclusion on an explicit value set needs no redraw at all: the domain is finite, so the values are removed at declaration and emptying it is a conflict there. - Conflicting constraints fail fast with a clear, actionable
ConflictingAnyConstraintExceptionat the moment the conflicting constraint is declared — for exampleAny.String().WithLength(3).StartingWith("ORD-"). - Dummies stay ordinary unless you ask for more. An unconstrained
Any.Double(),Single()orDecimal()draws within a magnitude of a million, not across the type's whole domain — so arithmetic on a dummy stays finite,WithScale(2)still has decimal places to constrain, and the value sits where rounding and formatting defects actually live. The window only ever clips:Between(0, double.MaxValue)permits a huge value and still yields an ordinary one, whileBetween(1e300, 1e308)names a magnitude and gets exactly it.Half, whose domain stops at 65 504, is unaffected. The integer generators deliberately keep their full range — a largeintis an ordinaryint. - Dummies stay small unless you ask for more. A bound is a permission, not a
request:
WithMaxLength/WithMaxCountonly ever narrow a draw, soAny.String().WithMaxLength(100_000)still yields the short unconstrained string rather than one sized after the cap. Only a minimum, an exact size or a required fragment enlarges a value —WithMinLength(90_000)is how you ask for a large one.WithLengthBetween(a, b)is exactly its two bounds declared separately, so a range starting at zero reads as a limit, not as a request to spread across it. A size the generator must actually produce is capped at 1 000 000: past that,WithLength/WithMinLength/WithCount/WithMinCountraise anArgumentOutOfRangeExceptionnaming your own parameter, instead of hanging or exhausting memory. A pure maximum is never capped — mirror a four-million-character column limit if you like; it costs nothing to honour. - Composition without reflection:
.As(factory)turns a constrained primitive into a domain value object;Any.Combine(...)assembles larger objects through constructor lambdas — from two up to eight constrained parts. - Collections over any element generator:
Any.ListOf(item),ArrayOf,SequenceOf,SetOfandDictionaryOf, constrained withWithCount/NonEmpty/Distinct/Containing. Ask a distinct collection for more distinct elements than its effective domain — the element generator plus any values pinned outside it withContaining— can supply, and it fails fast, just like any other conflict, wherever that domain is countable; where it is not, the same shortfall instead surfaces at generation as anAnyGenerationExceptionnaming the seed to replay.Any.PairOf/TripleOfpair generators into value tuples. - Optional values:
.OrNull()turns any generator into one that isnullabout half the time and otherwise a constrained value — the dummy for an optional field, for value types (int?,Guid?, ...) and reference types alike. - Reproducible runs: wrap a test in
Any.Reproducibly(...)and a failing run reports the seed to replay;Any.WithSeed(seed)gives an isolated, deterministic context;Any.UseSeed(seed)pins the ambient one until the handle is disposed, for a caller that has no body to wrap — a test-framework adapter driving the seed from before/after hooks. Its second overload names what the reader must write to replay, so a run pinned from outside the test body never points at a call the test does not contain. Drawing from several threads at once is safe — values stay arbitrary and well-formed — but concurrent draws interleave, so a seed replays a run only while its draws are taken one at a time; open anAny.UseSeed(...)scope per unit of work to keep a parallel run reproducible. A seed keeps its meaning across upgrades: within a major version, it draws the same values in every patch and minor, so a pinned seed committed today still covers the case it was pinned for after an upgrade. The mapping may change on a major version.
Example
using JustDummies;
OrderReference reference = Any.String()
.StartingWith("ORD-")
.WithLength(12)
.As(OrderReference.Create)
.Generate();
NaN and the infinities
Any.Double(), Any.Single() and Any.Half() never draw a non-finite value, and they
also refuse one as an argument — so Any.Double().Except(double.NaN) throws rather
than doing something sensible. That surprises people, so here is the whole rule and the
way out.
Why. An arbitrary test value should cross your invariants, not sabotage your
arithmetic. Every comparison with NaN is false, so a NaN drawn into an arrangement
the test never meant to exercise fails an assertion nobody wrote. Refusing it as an
argument too is the same decision applied one step earlier: a bound or an exclusion that
cannot be compared is not a constraint.
The way out — Any.OneOf. The generic entry points carry no finiteness rule, by
construction: Any.OneOf(...) takes your pool as the whole specification, and .As(...)
projects to whatever you return. The library judges the domains it knows; it does not
judge yours.
Which shape to use.
- The test asserts on the NaN path —
Rejects_a_non_finite_amount. Writedouble.NaNas a literal. It is the subject of the test, so it belongs at the call site in plain sight, not behind a generator. - A non-finite value is a real value of your domain —
NaNmeaning "missing measurement" in a scientific pipeline. ThenAny.OneOf(double.NaN, 1.0, 2.0)is right: it really is a dummy, drawn from the domain's real value set. - Avoid the middle ground. A mixed pool added "so the NaN path gets covered too" exercises that path on some seeds and not others, and the run does not tell you which branch it took. That is a test whose meaning depends on the draw.
If you do pool a NaN, know this. EqualityComparer<double>.Default.Equals(NaN, NaN)
is true while NaN == NaN is false. A NaN flowing through Distinct() — or any
comparer-based collection — therefore deduplicates, while your own == sees two
different values.
Where the rule stops. A typed builder — Any.Double(), Any.Enum<T>(), Any.String() — draws, and
accepts as an argument, only valid values of the domain it represents. The generic entry points do not, and
that is deliberate: Any.OneOf(...), Any.ElementOf(...) and .As(...) take your pool or your projection as
the whole specification, because the library cannot judge the semantics of a type it knows nothing about. So
Any.OneOf accepting what Any.Double() refuses is the design, not an oversight — it is the door the
paragraphs above send you through.
decimal is not part of this subject. System.Decimal has no NaN and no infinity
to begin with, so Any.Decimal() has nothing to refuse and carries no such guard. If you
went looking for the symmetry with Any.Double(), that is why you did not find it.
What it is not
No realistic fake data (names, emails, addresses), no object-graph auto-filling, no reflection. Small, deterministic, explicit.
And not a source of security material. Every draw comes from a seeded System.Random,
because a dummy is only worth generating if the seed a failing run reports replays it —
the very property that makes the sequence predictable to anyone who learns the seed.
Never draw a password, token, key, salt, nonce, or any identifier that has to be
unguessable from Any.*; reach for
System.Security.Cryptography.RandomNumberGenerator for those.
Documentation
Full documentation on GitHub:
https://github.com/Reefact/just-dummies
Credits
The package icon is a crash-test dummy by Magnific, from Flaticon.
| Product | Versions 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. |
-
.NETStandard 2.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on JustDummies:
| Package | Downloads |
|---|---|
|
JustDummies.Xunit
The xUnit v3 companion of JustDummies: mark a test, a class or an assembly [Reproducible] and its arbitrary values are drawn from a pinned seed, reported only when the test fails. Removes the per-test Any.Reproducibly ceremony without changing how values are generated. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.6 | 1,531 | 9/2/2026 |
| 1.0.0-preview.5 | 276 | 8/25/2026 |
| 1.0.0-preview.4 | 342 | 8/24/2026 |
| 1.0.0-preview.3 | 316 | 8/21/2026 |
| 1.0.0-preview.2 | 262 | 8/18/2026 |
| 1.0.0-preview.1 | 1,890 | 8/7/2026 |
| 0.1.0-preview.1 | 169 | 7/31/2026 |