# Using \`check-equal?\` with minor variations in expected data?

**URL:** <https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840>\
**Category:** Questions & Answers\
**Tags:** testing\
**Created:** [July 7, 2025, 8:18pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840 "2025-07-07T20:18:06Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [July 7, 2025, 8:18pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/1 "2025-07-07T20:18:06Z")

</div>

I have a test, where the expected results are slightly different when run under an older version of something. I needed to adapt the test to deal with both older and newer expected values.

So I (not proudly) changed the test from

```scheme
  (check-equal? action long-list-of-expected-values)

```

to something like:

```scheme
(check-true (or (equal? action long-list-of-expected-values)
                (equal? action almost-the-same-list)))

```

This works but it's [ugly and bad](https://github.com/greghendershott/racket-mode/commit/06fb64180f3f3727008e70aa62829ff00f473390).

1. One bad thing: `check-true`, when it fails, doesn't say anything very helpful -- whereas `check-equal?` shows both the actual and expected values. `check-equal?` is the spirit of what I want to keep using, ideally.

2. Another bad thing: All the repetition between the two lists -- they're mostly identical. Ideally I'd prefer some kind of "local `or`" in the middle of the data, to express the variation, that `check-equal?` cooperates with, somehow?

3. Starting to get OT, but it would be neat if `check-equal?` could do something like the `sexp-diff` library for actual vs diff. (I've bolted that on as a failure message, sometimes.)

Has anyone else dealt with this situation, more elegantly, and if so, how?

Although I have a couple ideas, I'm likely to either over- or under-think it. 😄

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [July 7, 2025, 8:27pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/2 "2025-07-07T20:27:29Z")

</div>

Oh, it sounds like I'm describing `check-match`, maybe?

I seem to recall trying that years ago and it didn't work out as well as I'd hoped. However it may have been pilot error. I'll take a look at that again.

Having said that, still would love to hear any ideas/experience people might have to share!

---

<div class="post-metadata">

**Author:** ![EmEf](https://avatars.discourse-cdn.com/v4/letter/e/53a042/32.png) [@EmEf](https://racket.discourse.group/u/EmEf)\
**Post date:** [July 7, 2025, 8:54pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/3 "2025-07-07T20:54:23Z")

</div>

Based on your description I’d try something like this:

```scheme
(define version 1)

(define (msg version) (~a "test 1 at version " version))

(check-equal? (action) (gen-expected version) (msg version)

```

If `match` is close enough to compare the expected lists, you should be able to generate the different expected results and use regular tests. (This is close to something I have done.)

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [July 8, 2025, 1:54am UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/4 "2025-07-08T01:54:57Z")

</div>

Thanks for the suggestion!

I think that makes a lot of sense when it's necessary or desirable to assert that specific versions produce specific results.

* * *

That's not quite my situation.

I'm usually designing tests that will pass when run against any version of Racket supported by Racket Mode.

And generally I don't need to check specific version numbers of Racket or certain packages. In fact I prefer to avoid it when possible.

My situation is closer to one where there is some small set of _similar_ OK values (where each could be a large-ish compound value, with relatively small differences from the others, and you'd really prefer to DRY/abstract ) and `random` chooses which one the test happens to get.

* * *

After working some more with `check-match`: Although it has a couple small rough edges, it's pretty good for this -- and definitely much better than my horrible `(check-true (or (equal? ____ ) ...))`.

---

<div class="post-metadata">

**Author:** ![EmEf](https://avatars.discourse-cdn.com/v4/letter/e/53a042/32.png) [@EmEf](https://racket.discourse.group/u/EmEf)\
**Post date:** [July 8, 2025, 11:50am UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/5 "2025-07-08T11:50:16Z")

</div>

Perhaps we should add `check-member-of` to rackunit. (It exists in the teaching languages and students learn to use it).

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [July 8, 2025, 12:53pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/6 "2025-07-08T12:53:44Z")

</div>

p.s. I alluded to some rough edges of `check-match` compared to `check-equal?`. Specifically:

- Most of the `check-xxx` functions have an optional `message` argument, but `check-match` instead has an optional `pred` argument. This means:

- Although `check-equal?` pretty-prints both the actual and expected values, `check-match` pretty-prints the `actual` but `display`s the pattern on whole line. This makes it very hard to grok -- much less compare/contrast with the actual (like you'd do when `check-equal?` fails).

Despite those paper cuts I think `check-match` is good enough, for me, for now.

* * *

For example a pattern like:

```scheme
(list
        (list 14 15 (hash-table ['invisible-open-count 1]
                                ['rhombus-type 'at]
                                ['type 'at]))
        (list 15 16 (hash-table ['rhombus-type 'operator]
                                ['type 'operator]))
        (list 16 17 (hash-table ['rhombus-type 'whitespace]
                                ['type 'white-space]))
        (list 17 18 (hash-table ['rhombus-type 'operator]
                                ['type 'operator]))
        (list 18 19 (hash-table ['rhombus-type 'opener]
                                ['type 'parenthesis]))
        (list 19 24 (or (hash-table
                         ['invisible-open-count 1]
                         ['rhombus-type 'identifier]
                         ['type 'symbol])
                        (hash-table
                         ['invisible-open-count 1]
                         ['rhombus-type 'identifier]
                         ['semantic-type-guess 'keyword] ;newer
                         ['type 'symbol])))
        (list 24 25 (hash-table ['rhombus-type 'whitespace]
                                ['type 'white-space]))
        (list 25 32 (hash-table ['invisible-close-count 1]
                                ['rhombus-type 'identifier]
                                ['type 'symbol]))
        (list 32 33 (hash-table ['invisible-close-count 1]
                                ['rhombus-type 'closer]
                                ['type 'parenthesis])))

```

ends up displayed as:

```scheme
pattern:
  (list (list 14 15 (hash-table ((quote invisible-open-count) 1) ((quote rhombus-type) (quote at)) ((quote type) (quote at)))) (list 15 16 (hash-table ((quote rhombus-type) (quote operator)) ((quote type) (quote operator)))) (list 16 17 (hash-table ((quote rhombus-type) (quote whitespace)) ((quote type) (quote white-space)))) (list 17 18 (hash-table ((quote rhombus-type) (quote operator)) ((quote type) (quote operator)))) (list 18 19 (hash-table ((quote rhombus-type) (quote opener)) ((quote type) (quote parenthesis)))) (list (quote XXXXX) 19 24 (or (hash-table ((quote invisible-open-count) 1) ((quote rhombus-type) (quote identifier)) ((quote type) (quote symbol))) (hash-table ((quote invisible-open-count) 1) ((quote rhombus-type) (quote identifier)) ((quote semantic-type-guess) (quote keyword)) ((quote type) (quote symbol))))) (list 24 25 (hash-table ((quote rhombus-type) (quote whitespace)) ((quote type) (quote white-space)))) (list 25 32 (hash-table ((quote invisible-close-count) 1) ((quote rhombus-type) (quote identifier)) ((quote type) (quote symbol)))) (list 32 33 (hash-table ((quote invisible-close-count) 1) ((quote rhombus-type) (quote closer)) ((quote type) (quote parenthesis)))))

```

---

<div class="post-metadata">

**Author:** ![LiberalArtist](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/liberalartist/32/151_2.png) [@LiberalArtist](https://racket.discourse.group/u/LiberalArtist)\
**Post date:** [July 8, 2025, 4:13pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/7 "2025-07-08T16:13:36Z")

</div>

> [@greghendershott](#):
>
> Although `check-equal?` pretty-prints both the actual and expected values, `check-match` pretty-prints the `actual` but `display`s the pattern on whole line. This makes it very hard to grok -- much less compare/contrast with the actual (like you'd do when `check-equal?` fails).

For better error reporting, you might express the expected value as a contract. You could then just use `check-not-exn` around a thunk using `contract` directly, though a `check-contract` form would be very nice.

For the specific example you gave, you'd probably want to write a contract combinator for record-like hashes. I've written many variants of that with various features over the years. I put a simple one in this example, but, looking at the one use of `or` in your `match` pattern, a combinator supporting optional keys might be even better for your use:

```scheme
#lang racket
(module+ main
  (list/c
   (list/c 14 15 (hash-of/c 'invisible-open-count 1
                            'rhombus-type 'at
                            'type 'at))
   (list/c 15 16 (hash-of/c 'rhombus-type 'operator
                            'type 'operator))
   (list/c 16 17 (hash-of/c 'rhombus-type 'whitespace
                            'type 'white-space))
   (list/c 17 18 (hash-of/c 'rhombus-type 'operator
                            'type 'operator))
   (list/c 18 19 (hash-of/c 'rhombus-type 'opener
                            'type 'parenthesis))
   (list/c 19 24 (or/c (hash-of/c
                        'invisible-open-count 1
                        'rhombus-type 'identifier
                        'type 'symbol)
                       (hash-of/c
                        'invisible-open-count 1
                        'rhombus-type 'identifier
                        'semantic-type-guess 'keyword ;newer
                        'type 'symbol)))
   (list/c 24 25 (hash-of/c 'rhombus-type 'whitespace
                            'type 'white-space))
   (list/c 25 32 (hash-of/c 'invisible-close-count 1
                            'rhombus-type 'identifier
                            'type 'symbol))
   (list/c 32 33 (hash-of/c 'invisible-close-count 1
                            'rhombus-type 'closer
                            'type 'parenthesis))))
(require racket/mutability)
(define (hash-of/c . orig-args)
  (let loop ([hsh #hash()]
             [pos 0]
             [args orig-args])
    (match args
      ['()
       (flat-hash-of/c hsh)]
      [(list* k v args)
       (when (hash-has-key? hsh k)
         (apply raise-argument-error 'hash-of/c "non-duplicate key" pos orig-args))
       (loop (hash-set hsh k (coerce-flat-contract 'hash-of/c v))
             (+ 2 pos)
             args)])))
(struct flat-hash-of/c (table)
  #:property prop:custom-write contract-custom-write-property-proc
  #:property prop:flat-contract
  (build-flat-contract-property
   #:name
   (match-lambda
     [(flat-hash-of/c table)
      (cons 'hash-of/c
            (append*
             (hash-map table
                       (λ (k v)
                         `(',k ,(contract-name v)))
                       'try-ordered)))])
   #:first-order
   (let ([missing (gensym)])
     (match-lambda
       [(flat-hash-of/c table)
        (λ (val)
          (and (immutable-hash? val)
               (for/and ([{k c} (in-immutable-hash table)])
                 (define v (hash-ref val k missing))
                 (and (not (eq? v missing))
                      (contract-first-order-passes? c v)))))]))
   #:late-neg-projection
   (match-lambda
     [(flat-hash-of/c table)
      (define late-neg-table
        (for/hash ([{k v} (in-immutable-hash table)])
          (values k (get/build-late-neg-projection v))))
      (λ (blame)
        (define keys-blame
          (blame-add-context blame "the keys of"))
        (define proj+blame-table
          (for/hash ([{k late-neg} (in-immutable-hash late-neg-table)])
            (values k (late-neg (blame-add-context blame (format "the value for ~e in"))))))
        (λ (val neg-party)
          (unless (immutable-hash? val)
            (raise-blame-error
             blame #:missing-party neg-party
             val
             `(expected: "an immutable hash table" given: "~e")
             val))
          (for ([{k proj+blame} (in-immutable-hash proj+blame-table)])
            (proj+blame
             (hash-ref val
                       k
                       (λ ()
                         (raise-blame-error
                          keys-blame #:missing-party neg-party
                          val
                          `("hash table is missing" expected "key"
                            expected: "~e" given: "~e")
                          k
                          (hash-keys val 'try-ordered))))
             neg-party))
          val))])))

```

* * *

For `check-match`, its implementation might want to use [`syntax->string`](https://docs.racket-lang.org/syntax/syntax-helpers.html#%28def._%28%28lib._syntax%2Fto-string..rkt%29._syntax-~3estring%29%29) on the pattern.

* * *

> [@greghendershott](#):
>
> My situation is closer to one where there is some small set of _similar_ OK values (where each could be a large-ish compound value, with relatively small differences from the others, and you'd really prefer to DRY/abstract ) and `random` chooses which one the test happens to get.

I like this solution less well for your use-case, but it got me thinking:

```scheme
#lang racket
(module+ main
  (alternatives
   (λ (amb)
     (list (amb #\a #\b #\c)
           (amb 1 2 3)
           (amb 'do 're 'mi)
           (amb #\a #\b #\c)
           (amb 1 2 3)
           (amb 'you 'me)))))
(define (alternatives proc)
  (define tag
    (make-continuation-prompt-tag 'alternates))
  (define (amb arg0 . args)
    (call/cc (λ (k)
               (abort-current-continuation
                tag
                (for/list ([v (in-list (cons arg0 args))])
                  (λ ()
                    (k v)))))
             tag))
  (let loop ([alts '()]
             [ks (list (λ ()
                         (values 'return (proc amb))))])
    (match ks
      ['()
       (reverse alts)]
      [(cons k ks)
       (match/values (call-with-continuation-prompt k
                                                    tag
                                                    (λ (more-ks)
                                                      (values 'abort more-ks)))
         [{'return rslt}
          (loop (cons rslt alts) ks)]
         [{'abort more-ks}
          (loop alts (append more-ks ks))])])))

```

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [July 9, 2025, 1:08pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/8 "2025-07-09T13:08:37Z")

</div>

@LiberalArtist Oh, that's an interesting idea to use a contract, and get better test failure messages.

Using `check-match` seems to be a more "natural" step from `check-equal?`. It's still in the spirit of, "Here's the expected value, which I originally copypasta-d from REPL output" 😄 -- just bending that to be more flexible using `match`'s pattern language.

However in reality, in this specific example, I had to modify that original REPL output a _lot_ (such as replacing all the `#hasheq` literals with `hash-table` patterns, quoting symbols, etc.).

It is _so_ unlike the original REPL output, in details, that it would hardly be much more work to express it as a contract -- and likely worth the effort to get better test failure messages.

I'll experiment with that; thanks!

---

<div class="post-metadata">

**Author:** ![LiberalArtist](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/liberalartist/32/151_2.png) [@LiberalArtist](https://racket.discourse.group/u/LiberalArtist)\
**Post date:** [July 9, 2025, 4:21pm UTC](https://racket.discourse.group/t/using-check-equal-with-minor-variations-in-expected-data/3840/9 "2025-07-09T16:21:56Z")

</div>

> [@greghendershott](#):
>
> However in reality, in this specific example, I had to modify that original REPL output a _lot_ (such as replacing all the `#hasheq` literals with `hash-table` patterns, quoting symbols, etc.).
> 
> It is _so_ unlike the original REPL output, in details, that it would hardly be much more work to express it as a contract -- and likely worth the effort to get better test failure messages.

I noticed this, too, when converting your `match` pattern to a contract, that is was mostly literal data, with `or/c` as an escape to specify a more complex case. Since contracts are first-class values, you could use a little helper function to mostly keep writing quasiquoted REPL output:

```scheme
(define/contract (datum->contract x)
  (-> (flat-rec-contract datum/c
        flat-contract?
        (listof datum/c)
        (hash/c any/c datum/c #:immutable #t))
      flat-contract?)
  (cond
    [(flat-contract? x) ; handles both atoms like 32 and embedded `or/c` etc.
     x]
    [(list? x)
     (apply list/c (map datum->contract x))]
    [(immutable-hash? x)
     (for/fold ([args '()]
                #:result (apply hash-of/c args))
               ([{k v} (in-immutable-hash x)])
       (list* k (datum->contract v) args))]))

(datum->contract
 `((14 15 #hasheq([invisible-open-count . 1]
                  [rhombus-type . at]
                  [type . at]))
   (15 16 #hasheq([rhombus-type . operator]
                  [type . operator]))
   (16 17 #hasheq([rhombus-type . whitespace]
                  [type . white-space]))
   (17 18 #hasheq([rhombus-type . operator]
                  [type . operator]))
   (18 19 #hasheq([rhombus-type . opener]
                  [type . parenthesis]))
   (19 24 ,(or/c (hash-of/c
                  'invisible-open-count 1
                  'rhombus-type 'identifier
                  'type 'symbol)
                 (hash-of/c
                  'invisible-open-count 1
                  'rhombus-type 'identifier
                  'semantic-type-guess 'keyword ;newer
                  'type 'symbol)))
   (24 25 #hasheq([rhombus-type . whitespace]
                  [type . white-space]))
   (25 32 #hasheq([invisible-close-count . 1]
                  [rhombus-type . identifier]
                  [type . symbol]))
   (32 33 #hasheq([invisible-close-count . 1]
                  [rhombus-type . closer]
                  [type . parenthesis]))))

```
