Skip to content

Cram Tests

Now that you have a set of unit tests for sandwich-related logic, you realize it would be nice to have some unit tests for burger-related logic as well. But adding a new command to your test npm script doesn’t feel like The OCaml Way, so you hop on the Reason Discord chatroom to ask for advice. MonadicFanatic1984 once again comes to your aid and points you toward cram tests.

Cram tests

A cram test is essentially a test that runs a command inside a sandbox, then compares the output with some expected output. Dune has built-in support for cram tests, which means:

  • You can specify dependencies for a cram test inside your dune file
  • Cram tests are only run when their dependencies change
  • When you update cram tests, causing their output to change, you can easily promote the latest output to become the new expected output
  • You can run all tests using a single command that doesn’t need to refer to individual tests

cram stanza

To add dependencies for your cram tests, you need to add a cram stanza to src/order-confirmation/dune:

dune
(cram
 (deps ./output/src/order-confirmation/SandwichTests.mjs))
(cram
 (deps ./output/src/order-confirmation/SandwichTests.mjs))

This configuration tells Dune two things:

  • Copy ./output/src/order-confirmation/SandwichTests.mjs into the sandbox directory
  • Whenever SandwichTests.re (the immediate dependency for ./output/src/order-confirmation/SandwichTests.mjs) changes, rebuild and re-run all the cram tests defined in the same directory as this dune file

Note that we cannot use the same path to SandwichTests.mjs as we would if we were running this from the root directory of our project, because cram tests are run inside a sandbox directory (more on that later).

Add tests.t file

Cram tests are specified inside .t files. Add a new file src/order-confirmation/tests.t:

cram
Sandwich tests
  $ node ./output/src/order-confirmation/SandwichTests.mjs
Sandwich tests
  $ node ./output/src/order-confirmation/SandwichTests.mjs

Note that this is the same path for SandwichTests.mjs that we specified inside the cram/deps field.

To run tests, execute

shell
dune build @runtest
dune build @runtest

Here, @runtest is a built-in alias. The above command builds whatever is needed by the tests and then runs the tests.

Aliases

A Dune alias is a virtual build target that other build targets can attach to. You can refer to aliases by name in build commands (like the one above), to build a specific subset of your build targets. In commands, we need to put @ in front of the alias name to distinguish it from file names. Therefore when we refer to an alias, we put an @ in front, e.g. @runtest, @melange, etc.

TIP

Cram tests are automatically attached to the @runtest alias, and melange.emit stanzas are automatically attached to the @melange alias.

Promotion

After running dune build @runtest, you should be able to see the output from the cram test you just added[1]:

diff
@@ -1,2 +1,27 @@
 Sandwich tests
   $ node ./output/src/cram-tests/SandwichTests.mjs
+  TAP version 13
+  # Subtest: Item.Sandwich.toEmoji
+  ok 1 - Item.Sandwich.toEmoji
+    ---
+    duration_ms: 2.173649
+    ...
+  # Subtest: Item.Sandwich.toPrice
+  ok 2 - Item.Sandwich.toPrice
+    ---
+    duration_ms: 0.216146
+    ...
+  # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
+  ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
+    ---
+    duration_ms: 0.106589
+    ...
+  1..3
+  # tests 3
+  # suites 0
+  # pass 3
+  # fail 0
+  # cancelled 0
+  # skipped 0
+  # todo 0
+  # duration_ms 0.111087
@@ -1,2 +1,27 @@
 Sandwich tests
   $ node ./output/src/cram-tests/SandwichTests.mjs
+  TAP version 13
+  # Subtest: Item.Sandwich.toEmoji
+  ok 1 - Item.Sandwich.toEmoji
+    ---
+    duration_ms: 2.173649
+    ...
+  # Subtest: Item.Sandwich.toPrice
+  ok 2 - Item.Sandwich.toPrice
+    ---
+    duration_ms: 0.216146
+    ...
+  # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
+  ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
+    ---
+    duration_ms: 0.106589
+    ...
+  1..3
+  # tests 3
+  # suites 0
+  # pass 3
+  # fail 0
+  # cancelled 0
+  # skipped 0
+  # todo 0
+  # duration_ms 0.111087

However, this doesn’t count as a passing cram test, because this output can’t be compared to any expected output. Dune has a nice feature that lets you promote the latest output of a cram test to become the expected output:

shell
dune promote
dune promote

The above command updates your src/order-confirmation/tests.t cram test file, which now looks something like this:

cram
Sandwich tests
  $ node ./output/src/order-confirmation/SandwichTests.mjs
  TAP version 13
  # Subtest: Item.Sandwich.toEmoji
  ok 1 - Item.Sandwich.toEmoji
    ---
    duration_ms: 1.821041
    ...
  # Subtest: Item.Sandwich.toPrice
  ok 2 - Item.Sandwich.toPrice
    ---
    duration_ms: 0.731916
    ...
  # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
  ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
    ---
    duration_ms: 0.086875
    ...
  1..3
  # tests 3
  # suites 0
  # pass 3
  # fail 0
  # cancelled 0
  # skipped 0
  # todo 0
  # duration_ms 0.081042
Sandwich tests
  $ node ./output/src/order-confirmation/SandwichTests.mjs
  TAP version 13
  # Subtest: Item.Sandwich.toEmoji
  ok 1 - Item.Sandwich.toEmoji
    ---
    duration_ms: 1.821041
    ...
  # Subtest: Item.Sandwich.toPrice
  ok 2 - Item.Sandwich.toPrice
    ---
    duration_ms: 0.731916
    ...
  # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
  ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
    ---
    duration_ms: 0.086875
    ...
  1..3
  # tests 3
  # suites 0
  # pass 3
  # fail 0
  # cancelled 0
  # skipped 0
  # todo 0
  # duration_ms 0.081042

The part underneath the command is the expected output of the command.

Add npm scripts

Instead of typing dune commands over and over, let’s add the relevant npm scripts to your package.json file. Replace the current test script with these three scripts:

json
"test": "npm run build -- @runtest",
"test:watch": "npm run build -- @runtest --watch",
"promote": "npm run dune -- promote"
"test": "npm run build -- @runtest",
"test:watch": "npm run build -- @runtest --watch",
"promote": "npm run dune -- promote"

Since JSON doesn’t support comments, you can optionally add some lines to the scriptsComments section of package.json to explain what each command does:

json
"test": "# Run the tests",
"test:watch": "# Watch files and re-run tests",
"promote": "# Promote most recent output to expected output"
"test": "# Run the tests",
"test:watch": "# Watch files and re-run tests",
"promote": "# Promote most recent output to expected output"

Run npm run test to check that it works.

Sanitize cram test output

The cram test still doesn’t succeed, but now it’s because the most recent output and the expected output don’t match:

diff
@@ -4,17 +4,17 @@ Sandwich tests
   # Subtest: Item.Sandwich.toEmoji
   ok 1 - Item.Sandwich.toEmoji
     ---
-    duration_ms: 2.225448
+    duration_ms: 3.012631
     ...
   # Subtest: Item.Sandwich.toPrice
   ok 2 - Item.Sandwich.toPrice
     ---
-    duration_ms: 0.262691
+    duration_ms: 0.276333
     ...
   # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
   ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
     ---
-    duration_ms: 0.109477
+    duration_ms: 0.140118
     ...
   1..3
   # tests 3
@@ -24,4 +24,4 @@ Sandwich tests
   # cancelled 0
   # skipped 0
   # todo 0
-  # duration_ms 0.125331
+  # duration_ms 0.143172
@@ -4,17 +4,17 @@ Sandwich tests
   # Subtest: Item.Sandwich.toEmoji
   ok 1 - Item.Sandwich.toEmoji
     ---
-    duration_ms: 2.225448
+    duration_ms: 3.012631
     ...
   # Subtest: Item.Sandwich.toPrice
   ok 2 - Item.Sandwich.toPrice
     ---
-    duration_ms: 0.262691
+    duration_ms: 0.276333
     ...
   # Subtest: Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
   ok 3 - Item.Sandwich.toPrice returns lower price for Turducken on Tuesdays
     ---
-    duration_ms: 0.109477
+    duration_ms: 0.140118
     ...
   1..3
   # tests 3
@@ -24,4 +24,4 @@ Sandwich tests
   # cancelled 0
   # skipped 0
   # todo 0
-  # duration_ms 0.125331
+  # duration_ms 0.143172

The problem is that the recorded durations for individual unit tests is a little bit different on each test run. Therefore we must sanitize the test output, i.e. remove the parts that are non-deterministic. Update your cram test command:

shell
$ node ./output/src/order-confirmation/SandwichTests.mjs | sed '/duration_ms/d'
$ node ./output/src/order-confirmation/SandwichTests.mjs | sed '/duration_ms/d'

Now the output of the command is piped to sed '/duration_ms/d', which removes all the lines containing the string duration_ms.

Follow these steps to achieve your first successful cram test run:

  1. Run npm run test:watch
  2. It shows errors because the expected output still contains the duration_ms lines
  3. Run npm run promote in another terminal to promote the latest output to the expected output
  4. You should see Success, waiting for filesystem changes...

No test output means it succeeded. Somewhat unintuitively, when cram tests succeed, they remain silent![2]

Sandbox

All cram tests execute in a sandbox. What goes into the sandbox directory is determined by the value of the cram/deps field in your dune file. Let’s write an explorative cram test that allows us to peek inside the sandbox. First, install the tree-node-cli package:

shell
npm install --save-dev tree-node-cli
npm install --save-dev tree-node-cli

Then add a new cram test to src/order-confirmation/tests.t:

cram
See all the files inside the sandbox
  $ npx tree
See all the files inside the sandbox
  $ npx tree

If npm run test:watch is still running, you’ll immediately see some output like this:

diff
@@ -24,3 +24,8 @@ Sandwich tests

 See all the files inside the sandbox
   $ npx tree
+  order-confirmation
+  └── output
+      └── src
+          └── order-confirmation
+              └── SandwichTests.mjs
@@ -24,3 +24,8 @@ Sandwich tests

 See all the files inside the sandbox
   $ npx tree
+  order-confirmation
+  └── output
+      └── src
+          └── order-confirmation
+              └── SandwichTests.mjs

The sandbox directory only contains a single file output/src/order-confirmation/SandwichTests.mjs. Add another explorative cram test:

cram
Show detail of SandwichTests.mjs
  $ ls -la ./output/src/order-confirmation/SandwichTests.mjs
Show detail of SandwichTests.mjs
  $ ls -la ./output/src/order-confirmation/SandwichTests.mjs

The output should now look like this:

diff
@@ -24,6 +24,12 @@ Sandwich tests

 See all the files inside the sandbox
   $ npx tree
+  cram-tests
+  └── output
+      └── src
+          └── cram-tests
+              └── SandwichTests.mjs

 Show detail of SandwichTests.mjs
   $ ls -la ./output/src/order-confirmation/SandwichTests.mjs
+  lrwxr-xr-x  1 fhsu  staff  86 Mar 16 22:09 ./output/src/order-confirmation/SandwichTests.mjs -> ../../../../../../../../default/src/order-confirmation/output/src/order-confirmation/SandwichTests.mjs
@@ -24,6 +24,12 @@ Sandwich tests

 See all the files inside the sandbox
   $ npx tree
+  cram-tests
+  └── output
+      └── src
+          └── cram-tests
+              └── SandwichTests.mjs

 Show detail of SandwichTests.mjs
   $ ls -la ./output/src/order-confirmation/SandwichTests.mjs
+  lrwxr-xr-x  1 fhsu  staff  86 Mar 16 22:09 ./output/src/order-confirmation/SandwichTests.mjs -> ../../../../../../../../default/src/order-confirmation/output/src/order-confirmation/SandwichTests.mjs

From this, you can see that ./output/src/order-confirmation/SandwichTests.mjs is actually a symbolic link which links to the real file inside your build directory, specifically this file:

_build/default/src/order-confirmation/output/src/order-confirmation/SandwichTests.mjs
_build/default/src/order-confirmation/output/src/order-confirmation/SandwichTests.mjs

SandwichTests.mjs being a symbolic link explains why you can run the test successfully even when the rest of the .mjs files aren’t present in the sandbox directory.

Better dependencies

There’s a problem with the cram test dependencies. The cram test is re-run if SandwichTests.re changes, but if Item.re (which SandwichTests.re depends on), were to change, it wouldn’t trigger a test re-run. To see this, make the following change to Item.Sandwich.toEmoji:

reason
let toEmoji = t =>
  Printf.sprintf(
    {js|🥪(%s)|js},
    switch (t) {
    | Portabello => {js|🍄|js}
    | Ham => {js|🐷|js}
    | Unicorn => {js|🦄|js}
    | Turducken => {js|🦃🦆🐓|js} 
    | Turducken => {js|🦃🦆🐓💩|js} 
    },
  );
let toEmoji = t =>
  Printf.sprintf(
    {js|🥪(%s)|js},
    switch (t) {
    | Portabello => {js|🍄|js}
    | Ham => {js|🐷|js}
    | Unicorn => {js|🦄|js}
    | Turducken => {js|🦃🦆🐓|js} 
    | Turducken => {js|🦃🦆🐓💩|js} 
    },
  );

In the terminal in which npm run test:watch is running, you would expect to see a failing test, but no errors appear. You have to add (alias melange) to cram/deps:

dune
(cram
 (deps
  (alias melange)
  ./output/src/order-confirmation/SandwichTests.mjs))
(cram
 (deps
  (alias melange)
  ./output/src/order-confirmation/SandwichTests.mjs))

Now the test finally fails. The presence of (alias melange) means that all build targets attached to the @melange alias are now dependencies for the cram tests in this directory.

expand_aliases_in_sandbox stanza

If you kept the $ npx tree cram test around, you’ll observe that even though all build targets attached to @melange are dependencies, they aren’t copied into the sandbox directory. You can change that behavior by adding the expand_aliases_in_sandbox stanza to your dune-project file:

dune
; Copy all build targets for an alias into the sandbox
(expand_aliases_in_sandbox)
; Copy all build targets for an alias into the sandbox
(expand_aliases_in_sandbox)

The npx tree cram test makes it clear that a lot of files have been copied over to the sandbox directory. Since SandwichTests.mjs is a @melange build target too, it is redundant and can be removed from cram/deps:

(cram
 (deps
  (alias melange)))
(cram
 (deps
  (alias melange)))

Fix the bug in Item.re. The SandwichTests cram test should pass once more.

WARNING

Using expand_aliases_in_sandbox is very convenient, but it may noticeably impact cram test performance. If you feel your cram tests are too slow, you should remove it and go back to putting .mjs files in your cram/deps field.


Fantastico! You’ve set up Dune cram tests to streamline the testing process. In the next chapter, we’ll add logic for promotional discounts.

Overview

  • A cram test runs a command inside a sandbox and compares the output of that command with some expected output
  • Dune has extensive support for cram tests, including the ability to promote the latest output to become the expected output
  • A cram stanza is used to specify dependencies for your cram tests
  • Cram test files have the .t extension
  • Aliases are virtual build targets that other build targets can be attached to
  • Cram tests that produce nondeterministic output must have their output be sanitized
  • Cram tests execute in a sandbox
  • For Melange projects, (alias melange) is generally the best dependency for your cram tests
  • The expand_aliases_in_sandbox stanza allows you to avoid having to put generated .mjs files in your cram test dependencies, at the cost of having slower cram tests

Exercises

1. Add new source file src/order-confirmation/BurgerTests.re:

re
open Fest;

test("A fully-loaded burger", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         onions: 2,
         cheese: 3,
         tomatoes: true,
         bacon: 4,
       }),
       {js|🍔|js},
     )
);
open Fest;

test("A fully-loaded burger", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         onions: 2,
         cheese: 3,
         tomatoes: true,
         bacon: 4,
       }),
       {js|🍔|js},
     )
);

Fix the broken logic inside the test and add a new cram test for BurgerTests in src/order-confirmation/tests.t.

Solution

Fixed test:

re
test("A fully-loaded burger", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         onions: 2,
         cheese: 3,
         tomatoes: true,
         bacon: 4,
       }),
       {js|🍔{🥬,🍅,🧅×2,🧀×3,🥓×4}|js},
     )
);
test("A fully-loaded burger", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         onions: 2,
         cheese: 3,
         tomatoes: true,
         bacon: 4,
       }),
       {js|🍔{🥬,🍅,🧅×2,🧀×3,🥓×4}|js},
     )
);

New cram test:

cram
Burger tests
  $ node ./output/src/order-confirmation/BurgerTests.mjs | sed '/duration_ms/d'
  TAP version 13
  # Subtest: A fully-loaded burger
  ok 1 - A fully-loaded burger
    ---
    ...
  1..1
  # tests 1
  # suites 0
  # pass 1
  # fail 0
  # cancelled 0
  # skipped 0
  # todo 0
Burger tests
  $ node ./output/src/order-confirmation/BurgerTests.mjs | sed '/duration_ms/d'
  TAP version 13
  # Subtest: A fully-loaded burger
  ok 1 - A fully-loaded burger
    ---
    ...
  1..1
  # tests 1
  # suites 0
  # pass 1
  # fail 0
  # cancelled 0
  # skipped 0
  # todo 0

2. Add three more tests to BurgerTests module:

reason
test("Burger with 0 of onions, cheese, or bacon doesn't show those emoji", () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);

test(
  "Burger with 1 of onions, cheese, or bacon should show just the emoji without ×",
  () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);

test("Burger with 2 or more of onions, cheese, or bacon should show ×", () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);
test("Burger with 0 of onions, cheese, or bacon doesn't show those emoji", () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);

test(
  "Burger with 1 of onions, cheese, or bacon should show just the emoji without ×",
  () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);

test("Burger with 2 or more of onions, cheese, or bacon should show ×", () =>
  expect |> equal(Item.Burger.toEmoji(/* burger record */), {js|🍔|js})
);

Get the tests to pass and promote the new test output.

Solution

Fixed tests:

re
test("Burger with 0 of onions, cheese, or bacon doesn't show those emoji", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 0,
         cheese: 0,
         bacon: 0,
       }),
       {js|🍔{🥬,🍅}|js},
     )
);

test(
  "Burger with 1 of onions, cheese, or bacon should show just the emoji without ×",
  () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 1,
         cheese: 1,
         bacon: 1,
       }),
       {js|🍔{🥬,🍅,🧅,🧀,🥓}|js},
     )
);

test("Burger with 2 or more of onions, cheese, or bacon should show ×", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 2,
         cheese: 2,
         bacon: 2,
       }),
       {js|🍔{🥬,🍅,🧅×2,🧀×2,🥓×2}|js},
     )
);
test("Burger with 0 of onions, cheese, or bacon doesn't show those emoji", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 0,
         cheese: 0,
         bacon: 0,
       }),
       {js|🍔{🥬,🍅}|js},
     )
);

test(
  "Burger with 1 of onions, cheese, or bacon should show just the emoji without ×",
  () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 1,
         cheese: 1,
         bacon: 1,
       }),
       {js|🍔{🥬,🍅,🧅,🧀,🥓}|js},
     )
);

test("Burger with 2 or more of onions, cheese, or bacon should show ×", () =>
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 2,
         cheese: 2,
         bacon: 2,
       }),
       {js|🍔{🥬,🍅,🧅×2,🧀×2,🥓×2}|js},
     )
);

3. Recently, some Cafe Emoji customers have gotten into the habit of ordering burgers with an absurd number of toppings. You can barely even hold these monstrous burgers in your hands, and on the small chance that you manage to grab one, it will explode as soon as you bite into it. Madame Jellobutter has therefore decided that whenever someone orders a burger with more than 12 toppings, it will be served in a big bowl. Add the following test to BurgerTests:

re
test("Burger with more than 12 toppings should also show bowl emoji", () => {
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 4,
         cheese: 2,
         bacon: 5,
       }),
       {js|🍔🥣{🥬,🍅,🧅×4,🧀×2,🥓×5}|js},
     );

  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 4,
         cheese: 2,
         bacon: 4,
       }),
       {js|🍔{🥬,🍅,🧅×4,🧀×2,🥓×4}|js},
     );
});
test("Burger with more than 12 toppings should also show bowl emoji", () => {
  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 4,
         cheese: 2,
         bacon: 5,
       }),
       {js|🍔🥣{🥬,🍅,🧅×4,🧀×2,🥓×5}|js},
     );

  expect
  |> equal(
       Item.Burger.toEmoji({
         lettuce: true,
         tomatoes: true,
         onions: 4,
         cheese: 2,
         bacon: 4,
       }),
       {js|🍔{🥬,🍅,🧅×4,🧀×2,🥓×4}|js},
     );
});

Now update Item.Burger.toEmoji to make that test pass.

Hint

Note that lettuce and tomatoes each count as 1 topping.

Solution

New version of Item.Burger.toEmoji:

re
let toEmoji = t => {
  let multiple = (emoji, count) =>
    switch (count) {
    | 0 => ""
    | 1 => emoji
    | count => Printf.sprintf({js|%s×%d|js}, emoji, count)
    };

  switch (t) {
  | {lettuce: false, onions: 0, cheese: 0, tomatoes: false, bacon: 0} => {js|🍔|js}
  | {lettuce, onions, cheese, tomatoes, bacon} =>
    let toppingsCount =
      (lettuce ? 1 : 0) + (tomatoes ? 1 : 0) + onions + cheese + bacon;

    Printf.sprintf(
      {js|🍔%s{%s}|js},
      toppingsCount > 12 ? {js|🥣|js} : "",
      [|
        lettuce ? {js|🥬|js} : "",
        tomatoes ? {js|🍅|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
        multiple({js|🥓|js}, bacon),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };
let toEmoji = t => {
  let multiple = (emoji, count) =>
    switch (count) {
    | 0 => ""
    | 1 => emoji
    | count => Printf.sprintf({js|%s×%d|js}, emoji, count)
    };

  switch (t) {
  | {lettuce: false, onions: 0, cheese: 0, tomatoes: false, bacon: 0} => {js|🍔|js}
  | {lettuce, onions, cheese, tomatoes, bacon} =>
    let toppingsCount =
      (lettuce ? 1 : 0) + (tomatoes ? 1 : 0) + onions + cheese + bacon;

    Printf.sprintf(
      {js|🍔%s{%s}|js},
      toppingsCount > 12 ? {js|🥣|js} : "",
      [|
        lettuce ? {js|🥬|js} : "",
        tomatoes ? {js|🍅|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
        multiple({js|🥓|js}, bacon),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };

View source code and demo for this chapter.



  1. You might notice that the output from Node test runner looks different when run inside a cram test. That’s because it uses different test reporters depending on whether you run it directly or through another process. ↩︎

  2. A successful cram test won’t print any output from the test, but you may see some logging that indicates that build targets were produced, e.g.

    shell
    refmt src/order-confirmation/Item.re.ml
    ppx src/order-confirmation/Item.re.pp.ml
    ocamldep src/order-confirmation/.output.mobjs/melange__Item.impl.d
    melc src/order-confirmation/.output.mobjs/melange/melange__Item.{cmi,cmj,cmt}
    melc src/order-confirmation/output/src/order-confirmation/Item.mjs
    melc src/order-confirmation/.output.mobjs/melange/melange__SandwichTests.{cmi,cmj,cmt}
    melc src/order-confirmation/.output.mobjs/melange/melange__Order.{cmi,cmj,cmt}
    melc src/order-confirmation/.output.mobjs/melange/melange__Index.{cmi,cmj,cmt}
    refmt src/order-confirmation/Item.re.ml
    ppx src/order-confirmation/Item.re.pp.ml
    ocamldep src/order-confirmation/.output.mobjs/melange__Item.impl.d
    melc src/order-confirmation/.output.mobjs/melange/melange__Item.{cmi,cmj,cmt}
    melc src/order-confirmation/output/src/order-confirmation/Item.mjs
    melc src/order-confirmation/.output.mobjs/melange/melange__SandwichTests.{cmi,cmj,cmt}
    melc src/order-confirmation/.output.mobjs/melange/melange__Order.{cmi,cmj,cmt}
    melc src/order-confirmation/.output.mobjs/melange/melange__Index.{cmi,cmj,cmt}
    ↩︎