<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: devtome</title>
    <description>The latest articles on DEV Community by devtome (@devtome).</description>
    <link>https://dev.to/devtome</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4107315%2F9c8b936a-b165-44dc-bb8a-3f9554b65a79.png</url>
      <title>DEV Community: devtome</title>
      <link>https://dev.to/devtome</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devtome"/>
    <language>en</language>
    <item>
      <title>How to Test a Randomized Web Experience Without Writing Flaky Tests</title>
      <dc:creator>devtome</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:46:02 +0000</pubDate>
      <link>https://dev.to/devtome/how-to-test-a-randomized-web-experience-without-writing-flaky-tests-ef7</link>
      <guid>https://dev.to/devtome/how-to-test-a-randomized-web-experience-without-writing-flaky-tests-ef7</guid>
      <description>&lt;p&gt;This post focuses on the testing techniques behind randomized interfaces rather than presenting an independent product review.&lt;/p&gt;

&lt;p&gt;Randomness is useful in many web experiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Card games&lt;/li&gt;
&lt;li&gt;Quiz question selection&lt;/li&gt;
&lt;li&gt;Recommendation prototypes&lt;/li&gt;
&lt;li&gt;Procedural illustrations&lt;/li&gt;
&lt;li&gt;Daily prompts&lt;/li&gt;
&lt;li&gt;Prize wheels&lt;/li&gt;
&lt;li&gt;Japanese &lt;a href="https://www.ichizenn.com/koi-mikuji/" rel="noopener noreferrer"&gt;恋みくじ&lt;/a&gt; fortune draws&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also an easy way to create unreliable tests.&lt;/p&gt;

&lt;p&gt;Consider this small function:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function selectRandom(items: T[]): T {&lt;br&gt;
  const index = Math.floor(Math.random() * items.length);&lt;br&gt;
  return items[index];&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The function looks too simple to cause problems. But how should we test it?&lt;/p&gt;

&lt;p&gt;This test is obviously incorrect:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
expect(selectRandom(["a", "b", "c"])).toBe("b");&lt;/p&gt;

&lt;p&gt;Sometimes it passes. Most of the time it does not.&lt;/p&gt;

&lt;p&gt;A more subtle mistake is repeating the function and expecting every possible result to appear:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const results = new Set(&lt;br&gt;
  Array.from(&lt;br&gt;
    { length: 20 },&lt;br&gt;
    () =&amp;gt; selectRandom(["a", "b", "c"])&lt;br&gt;
  )&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;expect(results).toEqual(new Set(["a", "b", "c"]));&lt;/p&gt;

&lt;p&gt;This will usually pass, which makes it more dangerous. A random test that “usually passes” is still a flaky test.&lt;/p&gt;

&lt;p&gt;The goal is not to remove randomness from the product. It is to remove uncontrolled randomness from the test environment.&lt;/p&gt;

&lt;p&gt;Separate decision logic from the random source&lt;/p&gt;

&lt;p&gt;The first improvement is to stop calling &lt;code&gt;Math.random()&lt;/code&gt; directly inside the selection logic.&lt;/p&gt;

&lt;p&gt;Instead, inject the source of randomness.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
type RandomSource = () =&amp;gt; number;&lt;/p&gt;

&lt;p&gt;function selectRandom(&lt;br&gt;
  items: T[],&lt;br&gt;
  random: RandomSource = Math.random&lt;br&gt;
): T {&lt;br&gt;
  if (items.length === 0) {&lt;br&gt;
    throw new Error("Cannot select from an empty collection");&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const index = Math.floor(random() * items.length);&lt;br&gt;
  return items[index];&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Production code can continue using the default:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const result = selectRandom(fortunes);&lt;/p&gt;

&lt;p&gt;Tests can supply predictable values:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
expect(&lt;br&gt;
  selectRandom(["a", "b", "c"], () =&amp;gt; 0)&lt;br&gt;
).toBe("a");&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
  selectRandom(["a", "b", "c"], () =&amp;gt; 0.5)&lt;br&gt;
).toBe("b");&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
  selectRandom(["a", "b", "c"], () =&amp;gt; 0.999)&lt;br&gt;
).toBe("c");&lt;/p&gt;

&lt;p&gt;The selection algorithm is now deterministic during testing.&lt;/p&gt;

&lt;p&gt;This pattern is a small form of dependency injection. Instead of hiding an external dependency inside the function, we pass it as an argument.&lt;/p&gt;

&lt;p&gt;Randomness is a dependency just like time, network access, storage, and environment variables.&lt;/p&gt;

&lt;p&gt;Test boundaries, not lucky outcomes&lt;/p&gt;

&lt;p&gt;When a random number is expected to be in the range &lt;code&gt;0 &amp;lt;= value &amp;lt; 1&lt;/code&gt;, the most useful tests are near the boundaries.&lt;/p&gt;

&lt;p&gt;For a three-item collection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; should select the first item.&lt;/li&gt;
&lt;li&gt;A value just below &lt;code&gt;1 / 3&lt;/code&gt; should select the first item.&lt;/li&gt;
&lt;li&gt;Exactly &lt;code&gt;1 / 3&lt;/code&gt; should select the second item.&lt;/li&gt;
&lt;li&gt;A value just below &lt;code&gt;2 / 3&lt;/code&gt; should select the second item.&lt;/li&gt;
&lt;li&gt;Exactly &lt;code&gt;2 / 3&lt;/code&gt; should select the third item.&lt;/li&gt;
&lt;li&gt;A value close to &lt;code&gt;1&lt;/code&gt; should select the final item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;typescript&lt;br&gt;
const values = ["first", "second", "third"];&lt;/p&gt;

&lt;p&gt;expect(selectRandom(values, () =&amp;gt; 0)).toBe("first");&lt;br&gt;
expect(selectRandom(values, () =&amp;gt; 0.332)).toBe("first");&lt;br&gt;
expect(selectRandom(values, () =&amp;gt; 1 / 3)).toBe("second");&lt;br&gt;
expect(selectRandom(values, () =&amp;gt; 0.665)).toBe("second");&lt;br&gt;
expect(selectRandom(values, () =&amp;gt; 2 / 3)).toBe("third");&lt;br&gt;
expect(selectRandom(values, () =&amp;gt; 0.999999)).toBe("third");&lt;/p&gt;

&lt;p&gt;These tests verify the mapping between the random value and the collection index.&lt;/p&gt;

&lt;p&gt;They do not attempt to prove that &lt;code&gt;Math.random()&lt;/code&gt; itself is random. That is not the responsibility of this unit.&lt;/p&gt;

&lt;p&gt;Decide how to handle invalid random sources&lt;/p&gt;

&lt;p&gt;The injected function creates another question: what happens if it returns an invalid value?&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
selectRandom(values, () =&amp;gt; 1);&lt;br&gt;
selectRandom(values, () =&amp;gt; -0.1);&lt;br&gt;
selectRandom(values, () =&amp;gt; Number.NaN);&lt;/p&gt;

&lt;p&gt;The standard &lt;code&gt;Math.random()&lt;/code&gt; function should not produce these values, but a custom source, mock, or future refactor might.&lt;/p&gt;

&lt;p&gt;One option is to reject invalid values explicitly:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function validateRandomValue(value: number): void {&lt;br&gt;
  if (!Number.isFinite(value) || value &amp;lt; 0 || value &amp;gt;= 1) {&lt;br&gt;
    throw new RangeError(&lt;br&gt;
      "Random source must return a finite number from 0 up to, but not including, 1"&lt;br&gt;
    );&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function selectRandom(&lt;br&gt;
  items: T[],&lt;br&gt;
  random: RandomSource = Math.random&lt;br&gt;
): T {&lt;br&gt;
  if (items.length === 0) {&lt;br&gt;
    throw new Error("Cannot select from an empty collection");&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const value = random();&lt;br&gt;
  validateRandomValue(value);&lt;/p&gt;

&lt;p&gt;return items[Math.floor(value * items.length)];&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Now invalid behavior fails immediately instead of producing an undefined result several steps later.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
expect(() =&amp;gt;&lt;br&gt;
  selectRandom(["a"], () =&amp;gt; 1)&lt;br&gt;
).toThrow(RangeError);&lt;/p&gt;

&lt;p&gt;Whether this validation belongs in production code depends on the application. But the behavior should be intentional and documented.&lt;/p&gt;

&lt;p&gt;Weighted selection needs deterministic tests too&lt;/p&gt;

&lt;p&gt;Some applications assign different weights to possible results.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
type WeightedItem = {&lt;br&gt;
  value: T;&lt;br&gt;
  weight: number;&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;A weighted selector might look like this:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function selectWeighted(&lt;br&gt;
  items: WeightedItem[],&lt;br&gt;
  random: RandomSource = Math.random&lt;br&gt;
): T {&lt;br&gt;
  if (items.length === 0) {&lt;br&gt;
    throw new Error("Cannot select from an empty collection");&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;const totalWeight = items.reduce(&lt;br&gt;
    (sum, item) =&amp;gt; sum + item.weight,&lt;br&gt;
    0&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;if (totalWeight &amp;lt;= 0) {&lt;br&gt;
    throw new Error("Total weight must be positive");&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;let position = random() * totalWeight;&lt;/p&gt;

&lt;p&gt;for (const item of items) {&lt;br&gt;
    position -= item.weight;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (position &amp;lt; 0) {
  return item.value;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;return items[items.length - 1].value;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Suppose the weights are:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const options = [&lt;br&gt;
  { value: "gentle", weight: 1 },&lt;br&gt;
  { value: "bright", weight: 2 },&lt;br&gt;
  { value: "reflective", weight: 1 }&lt;br&gt;
];&lt;/p&gt;

&lt;p&gt;The total weight is four.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gentle&lt;/code&gt; owns the interval from 0 to 1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bright&lt;/code&gt; owns the interval from 1 to 3.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reflective&lt;/code&gt; owns the interval from 3 to 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the random function returns values between zero and one, we can test each interval deliberately:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
expect(&lt;br&gt;
  selectWeighted(options, () =&amp;gt; 0)&lt;br&gt;
).toBe("gentle");&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
  selectWeighted(options, () =&amp;gt; 0.3)&lt;br&gt;
).toBe("bright");&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
  selectWeighted(options, () =&amp;gt; 0.74)&lt;br&gt;
).toBe("bright");&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
  selectWeighted(options, () =&amp;gt; 0.99)&lt;br&gt;
).toBe("reflective");&lt;/p&gt;

&lt;p&gt;Again, the test controls the input. It does not wait for the desired outcome to appear by chance.&lt;/p&gt;

&lt;p&gt;Weighted selection should also reject invalid data:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function validateWeights(&lt;br&gt;
  items: WeightedItem[]&lt;br&gt;
): void {&lt;br&gt;
  for (const item of items) {&lt;br&gt;
    if (!Number.isFinite(item.weight) || item.weight &amp;lt; 0) {&lt;br&gt;
      throw new Error("Weights must be finite and non-negative");&lt;br&gt;
    }&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Useful edge cases include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An empty collection&lt;/li&gt;
&lt;li&gt;All weights set to zero&lt;/li&gt;
&lt;li&gt;A negative weight&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;NaN&lt;/code&gt; weight&lt;/li&gt;
&lt;li&gt;One item with all available weight&lt;/li&gt;
&lt;li&gt;Zero-weight items between valid candidates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use seeded randomness for larger scenarios&lt;/p&gt;

&lt;p&gt;Injecting a fixed value is excellent for unit tests. Integration tests sometimes need a sequence of different but repeatable values.&lt;/p&gt;

&lt;p&gt;A seeded pseudo-random generator can provide that sequence.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function mulberry32(seed: number): RandomSource {&lt;br&gt;
  return function () {&lt;br&gt;
    let value = seed += 0x6d2b79f5;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value = Math.imul(value ^ (value &amp;gt;&amp;gt;&amp;gt; 15), value | 1);
value ^= value + Math.imul(
  value ^ (value &amp;gt;&amp;gt;&amp;gt; 7),
  value | 61
);

return (
  (value ^ (value &amp;gt;&amp;gt;&amp;gt; 14)) &amp;gt;&amp;gt;&amp;gt; 0
) / 4294967296;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;};&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The same seed produces the same sequence:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const randomA = mulberry32(12345);&lt;br&gt;
const randomB = mulberry32(12345);&lt;/p&gt;

&lt;p&gt;expect(randomA()).toBe(randomB());&lt;br&gt;
expect(randomA()).toBe(randomB());&lt;br&gt;
expect(randomA()).toBe(randomB());&lt;/p&gt;

&lt;p&gt;This makes complex scenarios reproducible.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const random = mulberry32(2026);&lt;/p&gt;

&lt;p&gt;const sequence = Array.from(&lt;br&gt;
  { length: 10 },&lt;br&gt;
  () =&amp;gt; selectRandom(fortunes, random)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;If the test fails, the seed can be printed in the error message. The developer can then reproduce exactly the same sequence.&lt;/p&gt;

&lt;p&gt;A seeded generator is useful for testing. It should not be mistaken for a cryptographically secure random source.&lt;/p&gt;

&lt;p&gt;For security-sensitive tokens, lotteries involving real value, or authentication systems, use an appropriate cryptographic API instead.&lt;/p&gt;

&lt;p&gt;Test invariants instead of exact sequences&lt;/p&gt;

&lt;p&gt;Many randomized systems have rules that should remain true for every possible outcome.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The selected result must belong to the eligible collection.&lt;/li&gt;
&lt;li&gt;A disabled result must never be selected.&lt;/li&gt;
&lt;li&gt;The function must return exactly one result.&lt;/li&gt;
&lt;li&gt;The original array must not be modified.&lt;/li&gt;
&lt;li&gt;A result with zero weight must never be returned.&lt;/li&gt;
&lt;li&gt;The selected locale must match the requested locale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are invariants.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const original = [...fortunes];&lt;br&gt;
const selected = selectRandom(fortunes, () =&amp;gt; 0.4);&lt;/p&gt;

&lt;p&gt;expect(fortunes).toEqual(original);&lt;br&gt;
expect(fortunes).toContain(selected);&lt;/p&gt;

&lt;p&gt;For contextual selection:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const selected = drawFortune({&lt;br&gt;
  fortunes,&lt;br&gt;
  situation: "waiting_for_reply",&lt;br&gt;
  locale: "en",&lt;br&gt;
  random: () =&amp;gt; 0.25&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;expect(selected.locale).toBe("en");&lt;br&gt;
expect(&lt;br&gt;
  selected.situations.includes("waiting_for_reply") ||&lt;br&gt;
  selected.situations.includes("general")&lt;br&gt;
).toBe(true);&lt;/p&gt;

&lt;p&gt;Invariants are more stable than testing a specific message. Content can change while the behavioral contract remains valid.&lt;/p&gt;

&lt;p&gt;Statistical tests are different from unit tests&lt;/p&gt;

&lt;p&gt;Sometimes we do need to check whether a weighted distribution behaves approximately as expected.&lt;/p&gt;

&lt;p&gt;For example, with weights &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt;, the long-run proportions should approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;25%&lt;/li&gt;
&lt;li&gt;50%&lt;/li&gt;
&lt;li&gt;25%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simulation can detect a serious implementation mistake:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function simulate(&lt;br&gt;
  iterations: number,&lt;br&gt;
  random: RandomSource&lt;br&gt;
) {&lt;br&gt;
  const counts = new Map();&lt;/p&gt;

&lt;p&gt;for (let i = 0; i &amp;lt; iterations; i++) {&lt;br&gt;
    const result = selectWeighted(options, random);&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;counts.set(
  result,
  (counts.get(result) ?? 0) + 1
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;return counts;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const counts = simulate(&lt;br&gt;
  100_000,&lt;br&gt;
  mulberry32(7890)&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;const brightRatio =&lt;br&gt;
  (counts.get("bright") ?? 0) / 100_000;&lt;/p&gt;

&lt;p&gt;expect(brightRatio).toBeGreaterThan(0.48);&lt;br&gt;
expect(brightRatio).toBeLessThan(0.52);&lt;/p&gt;

&lt;p&gt;There are two important details here.&lt;/p&gt;

&lt;p&gt;First, the simulation uses a fixed seed, so it produces the same result in every test run.&lt;/p&gt;

&lt;p&gt;Second, the tolerance is intentionally broader than an exact equality.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
// Do not do this&lt;br&gt;
expect(brightRatio).toBe(0.5);&lt;/p&gt;

&lt;p&gt;A distribution test should not run as an unpredictable experiment inside every pull request. It should be deterministic and used to detect large deviations.&lt;/p&gt;

&lt;p&gt;For systems where fairness has financial, legal, or competitive consequences, informal simulations are not enough. Those systems need specialist review and stronger statistical methods.&lt;/p&gt;

&lt;p&gt;Test the UI as a state machine&lt;/p&gt;

&lt;p&gt;A random experience is rarely only a selection function.&lt;/p&gt;

&lt;p&gt;The interface may move through several stages:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
type DrawState =&lt;br&gt;
  | "idle"&lt;br&gt;
  | "drawing"&lt;br&gt;
  | "revealed"&lt;br&gt;
  | "error";&lt;/p&gt;

&lt;p&gt;The expected transitions can be described explicitly:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
idle → drawing → revealed&lt;br&gt;
idle → drawing → error&lt;br&gt;
revealed → drawing → revealed&lt;/p&gt;

&lt;p&gt;Tests should verify behavior at every stage.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("disables the draw button during animation", async () =&amp;gt; {&lt;br&gt;
  render( 0.4} /&amp;gt;);&lt;/p&gt;

&lt;p&gt;const button = screen.getByRole("button", {&lt;br&gt;
    name: /draw/i&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;await user.click(button);&lt;/p&gt;

&lt;p&gt;expect(button).toBeDisabled();&lt;br&gt;
  expect(&lt;br&gt;
    screen.getByText(/drawing/i)&lt;br&gt;
  ).toBeVisible();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;After the result appears:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("reveals exactly one result", async () =&amp;gt; {&lt;br&gt;
  render( 0.4} /&amp;gt;);&lt;/p&gt;

&lt;p&gt;await user.click(&lt;br&gt;
    screen.getByRole("button", { name: /draw/i })&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;const results =&lt;br&gt;
    await screen.findAllByTestId("fortune-result");&lt;/p&gt;

&lt;p&gt;expect(results).toHaveLength(1);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The random source remains injected all the way into the UI component.&lt;/p&gt;

&lt;p&gt;This is important. Mocking only the lower-level function while allowing the component to call &lt;code&gt;Math.random()&lt;/code&gt; elsewhere can reintroduce unpredictable behavior.&lt;/p&gt;

&lt;p&gt;Control time as well as randomness&lt;/p&gt;

&lt;p&gt;Fortune draws often include a short reveal animation.&lt;/p&gt;

&lt;p&gt;Waiting for real timers slows the test suite and can cause timing failures.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function revealAfterDelay(&lt;br&gt;
  callback: () =&amp;gt; void,&lt;br&gt;
  delay = 1000&lt;br&gt;
) {&lt;br&gt;
  setTimeout(callback, delay);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;With fake timers, the test controls time:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("reveals the result after the animation", async () =&amp;gt; {&lt;br&gt;
  vi.useFakeTimers();&lt;/p&gt;

&lt;p&gt;render( 0.2} /&amp;gt;);&lt;/p&gt;

&lt;p&gt;await user.click(&lt;br&gt;
    screen.getByRole("button", { name: /draw/i })&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
    screen.queryByTestId("fortune-result")&lt;br&gt;
  ).not.toBeInTheDocument();&lt;/p&gt;

&lt;p&gt;await vi.advanceTimersByTimeAsync(1000);&lt;/p&gt;

&lt;p&gt;expect(&lt;br&gt;
    screen.getByTestId("fortune-result")&lt;br&gt;
  ).toBeVisible();&lt;/p&gt;

&lt;p&gt;vi.useRealTimers();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;Randomness and time are two separate dependencies. Both should be controllable.&lt;/p&gt;

&lt;p&gt;Test reduced-motion behavior&lt;/p&gt;

&lt;p&gt;A polished animation should not become a requirement for completing the interaction.&lt;/p&gt;

&lt;p&gt;When the visitor prefers reduced motion, the result can appear immediately or use a simpler transition.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function getRevealDelay(&lt;br&gt;
  prefersReducedMotion: boolean&lt;br&gt;
): number {&lt;br&gt;
  return prefersReducedMotion ? 0 : 1000;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("does not delay the result when reduced motion is preferred", () =&amp;gt; {&lt;br&gt;
  expect(getRevealDelay(true)).toBe(0);&lt;br&gt;
  expect(getRevealDelay(false)).toBe(1000);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The test should verify that both paths reach the same final result.&lt;/p&gt;

&lt;p&gt;Accessibility is not only a CSS concern. It can change application timing and state transitions.&lt;/p&gt;

&lt;p&gt;Test focus after the reveal&lt;/p&gt;

&lt;p&gt;When visual content changes, keyboard and screen-reader users need a clear indication of what happened.&lt;/p&gt;

&lt;p&gt;The result heading can receive focus after the draw:&lt;/p&gt;

&lt;p&gt;tsx&lt;br&gt;
&amp;lt;h2&lt;br&gt;
  ref={resultHeadingRef}&lt;br&gt;
  tabIndex={-1}&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;{result.headline}&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;p&gt;The test can verify that behavior:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("moves focus to the revealed result", async () =&amp;gt; {&lt;br&gt;
  render( 0.3} /&amp;gt;);&lt;/p&gt;

&lt;p&gt;await user.click(&lt;br&gt;
    screen.getByRole("button", { name: /draw/i })&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;const heading = await screen.findByRole(&lt;br&gt;
    "heading",&lt;br&gt;
    { level: 2 }&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;expect(heading).toHaveFocus();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;This test does not care which poetic sentence appears. It checks whether the interaction remains understandable.&lt;/p&gt;

&lt;p&gt;Prevent multiple draws from one action&lt;/p&gt;

&lt;p&gt;A fast double-click can expose race conditions.&lt;/p&gt;

&lt;p&gt;Without protection, two timers may start and two results may be recorded.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("accepts only one draw while a draw is in progress", async () =&amp;gt; {&lt;br&gt;
  const random = vi.fn(() =&amp;gt; 0.5);&lt;/p&gt;

&lt;p&gt;render();&lt;/p&gt;

&lt;p&gt;const button = screen.getByRole("button", {&lt;br&gt;
    name: /draw/i&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;await user.dblClick(button);&lt;/p&gt;

&lt;p&gt;expect(random).toHaveBeenCalledTimes(1);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;Disabling the button visually is not always enough. The event handler should also protect its state.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
function beginDraw() {&lt;br&gt;
  if (state !== "idle" &amp;amp;&amp;amp; state !== "revealed") {&lt;br&gt;
    return;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;setState("drawing");&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This is especially important when the draw creates analytics events, local history, or server requests.&lt;/p&gt;

&lt;p&gt;Test analytics without testing the analytics provider&lt;/p&gt;

&lt;p&gt;The product may record that a draw was completed.&lt;/p&gt;

&lt;p&gt;The test should verify the application’s event contract, not make a real request to an analytics service.&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
type TrackEvent = (&lt;br&gt;
  name: string,&lt;br&gt;
  properties?: Record&lt;br&gt;
) =&amp;gt; void;&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
test("records one completion event", async () =&amp;gt; {&lt;br&gt;
  const track = vi.fn();&lt;/p&gt;

&lt;p&gt;render(&lt;br&gt;
    
      random={() =&amp;gt; 0.6}&lt;br&gt;
      track={track}&lt;br&gt;
    /&amp;gt;&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;await user.click(&lt;br&gt;
    screen.getByRole("button", { name: /draw/i })&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;expect(track).toHaveBeenCalledTimes(1);&lt;br&gt;
  expect(track).toHaveBeenCalledWith(&lt;br&gt;
    "fortune_draw_completed",&lt;br&gt;
    expect.objectContaining({&lt;br&gt;
      locale: "en"&lt;br&gt;
    })&lt;br&gt;
  );&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;A test can also verify which information is not sent:&lt;/p&gt;

&lt;p&gt;typescript&lt;br&gt;
const properties = track.mock.calls[0][1];&lt;/p&gt;

&lt;p&gt;expect(properties).not.toHaveProperty("name");&lt;br&gt;
expect(properties).not.toHaveProperty("question");&lt;br&gt;
expect(properties).not.toHaveProperty("relationshipDetails");&lt;/p&gt;

&lt;p&gt;Testing privacy boundaries can be just as important as testing output.&lt;/p&gt;

&lt;p&gt;Applying these tests to a real experience&lt;/p&gt;

&lt;p&gt;I have been thinking about these testing patterns while working with &lt;a href="https://www.ichizenn.com/koi-mikuji/" rel="noopener noreferrer"&gt;Ichizenn’s 恋みくじ&lt;/a&gt;, a browser-based Japanese love-fortune experience.&lt;/p&gt;

&lt;p&gt;The interesting part is that a result should feel unpredictable to the visitor while remaining completely reproducible in the test suite.&lt;/p&gt;

&lt;p&gt;Those goals do not conflict.&lt;/p&gt;

&lt;p&gt;Production can use a genuine runtime random source. Tests can inject a controlled one. Both environments exercise the same selection rules, state transitions, accessibility behavior, and error handling.&lt;/p&gt;

&lt;p&gt;The test does not need to know whether the result says “wait patiently” or “take a small step forward.” It needs to know that the result was eligible, rendered once, announced correctly, and produced without exposing private context.&lt;/p&gt;

&lt;p&gt;A practical testing checklist&lt;/p&gt;

&lt;p&gt;For a randomized web interaction, I would test the following:&lt;/p&gt;

&lt;p&gt;Selection&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Empty collections fail clearly.&lt;/li&gt;
&lt;li&gt;Boundary random values select the correct items.&lt;/li&gt;
&lt;li&gt;Every selected item belongs to the eligible collection.&lt;/li&gt;
&lt;li&gt;Disabled and zero-weight results are never returned.&lt;/li&gt;
&lt;li&gt;The input collection is not modified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weighting&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interval boundaries map to the correct result.&lt;/li&gt;
&lt;li&gt;Negative and invalid weights are rejected.&lt;/li&gt;
&lt;li&gt;All-zero weights fail clearly.&lt;/li&gt;
&lt;li&gt;A seeded distribution test detects large implementation errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Interface state&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The draw button is disabled while drawing.&lt;/li&gt;
&lt;li&gt;Double-clicking does not create multiple draws.&lt;/li&gt;
&lt;li&gt;Success and error paths reach valid states.&lt;/li&gt;
&lt;li&gt;Timers are controlled in tests.&lt;/li&gt;
&lt;li&gt;A second draw does not leave the previous result active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Accessibility&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced-motion preferences are respected.&lt;/li&gt;
&lt;li&gt;Focus moves to the result.&lt;/li&gt;
&lt;li&gt;The result is available as real text.&lt;/li&gt;
&lt;li&gt;Status changes can be announced by assistive technology.&lt;/li&gt;
&lt;li&gt;The draw remains usable with a keyboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Privacy and analytics&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events are emitted once.&lt;/li&gt;
&lt;li&gt;Events use the expected names and properties.&lt;/li&gt;
&lt;li&gt;Personal questions are not included.&lt;/li&gt;
&lt;li&gt;Test logs do not contain private user input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final thoughts&lt;/p&gt;

&lt;p&gt;Testing a randomized interface does not require random tests.&lt;/p&gt;

&lt;p&gt;The most reliable strategy is to make uncertainty controllable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inject the random source.&lt;/li&gt;
&lt;li&gt;Use fixed values for unit tests.&lt;/li&gt;
&lt;li&gt;Use seeded sequences for larger scenarios.&lt;/li&gt;
&lt;li&gt;Test boundaries and invariants.&lt;/li&gt;
&lt;li&gt;Treat statistical checks separately.&lt;/li&gt;
&lt;li&gt;Control timers.&lt;/li&gt;
&lt;li&gt;Verify UI state and accessibility.&lt;/li&gt;
&lt;li&gt;Test what the application refuses to collect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visitors can still experience surprise.&lt;/p&gt;

&lt;p&gt;Developers should not experience surprise when the test suite runs.&lt;/p&gt;

&lt;p&gt;What other hidden dependencies—time, locale, network conditions, browser APIs, or feature flags—have caused flaky tests in your projects?&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
  </channel>
</rss>
