Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalized unary and binary assertions #7

Closed
briancavalier opened this issue Jan 26, 2017 · 5 comments
Closed

Generalized unary and binary assertions #7

briancavalier opened this issue Jan 26, 2017 · 5 comments

Comments

@briancavalier
Copy link
Owner

It seems like there are general forms of unary and binary assertions. Let's call them where and where2:

where :: (a -> boolean) -> a -> a
where2 :: (a -> b -> boolean) -> a -> b -> b

These lift unary and binary predicates into assertions. For example, if assertions are types, then where and where2 construct assertions from predicates:

type Assert a = a -> a
type Assert2 a b = a -> b -> b

type Predicate a = a -> boolean
type Predicate2 a b = a -> b -> boolean

where :: Predicate a -> Assert a
where2 :: Predicate2 a b -> Assert2 a b

Example

These could be a simple API for allowing easy creation of new assertions in user land. Say you want and instanceof assertion:

// instanceOf :: Predicate2 Function a
const instanceOf = (C, a) => a instanceof C

// Make a general instanceOf binary assertion, useful in its own right
// assertInstanceOf :: Assert2 Function a
const assertInstanceOf = where2(instanceOf)

// Further make a specific unary assertion for instanceof TypeError
// assertTypeError :: Assert a
const assertTypeError = assertInstanceOf(TypeError)

Problems

Generalizing things this far with the current "assertions are functions" approach makes it harder to provide specific failure messages. One way is to rely on the predicate function's name. As a simple example, could generate a string like: ${predicate.name}(${inspect(expected)}, ${inspect(actual)}), which is not terrible as long as predicate.name is reasonable.

Another option is to force a leading string arg to use as a prefix. That can also have problems when you do 2 separate levels of refinement, like above with the instanceOf -> assertInstanceOf -> assertTypeError. That particular example might actually end up with a reasonable failure message using predicate.name, but the general case may not.

@unscriptable
Copy link

What are your motivations for creating these generalized assertions? The one that came to me immediately (and you mentioned it, too) was better failure messages. Like most assertion libs, the assert() failure message is vague and forces me to work to find the source of the problem.

@briancavalier
Copy link
Owner Author

My primary motivation right now is extensibility, and not having to provide every possible assertion.

Failure messages are definitely a problem, and I think the big problem is using exceptions to represent failure. I have a ton of thoughts on using return values instead, but that's a convo for another time, because ... one of the goals for this project is to integrate easily with existing test frameworks, which already tend to use thrown exceptions as the assertion failure channel.

Because exceptions represent a different channel, they seem to force you to do one of these:

  1. Construct failure messages at the deepest level of the assertion library, where the code has the least overall context of what is going on. The lack of context makes it hard or impossible to construct really good failure messages ... a stack trace often seems to be more useful.
  2. Pass enough information all the way to the deepest level of the assertion library, such that it could construct good failure messages.
  3. Use the exception channel to accumulate information about failures, and construct messages higher up (by catching, accumulating context, and rethrowing) where more context is available. This basically requires try/catch all over the place inside the lib, which is 1) gross, and 2) causes most current JITs to give up on optimization.

There are tricks. For example, I tried doing something like better-assert. It's nice, but only works on v8. Maybe it's extensible to other VMs by parsing stack traces as text--seems like a rat hole. Also, these days, you need working sourcemaps since many folks are writing tests in ES2015, compiled on the fly by something like mocha -r buba/register (or babel-register, etc.).

Then there's power-assert, which requires a babel transform. It might not be a bad option to use a compile step, but it's asking a bit more of users. Maybe that's ok, since it could be opt-in for better messages.

Any thoughts?

@unscriptable
Copy link

I went through a very similar thought process last week, but I didn't bother researching other assertions libs. I just shrugged and then used node's built-in assert module for now. :)

power-assert is interesting. I like the descriptiveness and thoroughness of the failure messages. It already looks bloated, though. Well, I shouldn't judge it until I look closer, of course. :)

Perhaps I'll rekindle my efforts to build a babel-based doctest lib, which could output similar messages.

I think your ${predicate.name}(${inspect(expected)}, ${inspect(actual)}) is good enough and fits with your current strategy.

@briancavalier
Copy link
Owner Author

I just shrugged and then used node's built-in assert module for now

Yeah, I hear you. In a way, that's what I've ended up doing: make something simple that does what I want, works with the test framework I'm already using, and solves some pain points I've had with other assert libs.

This where stuff might even be too much, but I do want to find a simple way to make the lib extensible so it's a bit easier to use in cases where eq, is, and assert don't work elegantly.

Perhaps I'll rekindle my efforts to build a babel-based doctest lib

Sounds great. When can I use it? I know an assertion lib that might work well with it, btw ;)

I think your ${predicate.name}(${inspect(expected)}, ${inspect(actual)}) is good enough and fits with your current strategy

Agreed. It's probably good enough to implement a POC and see what it looks like.

Thanks for the feedback!

@unscriptable
Copy link

This where stuff might even be too much

I think it's just right, given your objectives/motivations. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants