Skip to content

Expand the doc comment for Context #1902

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

Merged
merged 27 commits into from
Feb 6, 2023
Merged

Conversation

natebosch
Copy link
Member

@natebosch natebosch commented Feb 3, 2023

This is the main point of interaction for authors of extensions, so docs
cover a wide range of information, including how the descriptions will
be used in failure output. Include some descriptions about how behavior
can vary across different contexts, and some high level descriptions
of the behavior for the contexts related to specific utilities.

The main benefit this brings is it brings more alignment with the
`clause` arguments for `expect` calls. The docs will be able to focus on
the difference in how the value is use (preceding "that" in the case of
labels, standing on its own in a list in the case of clauses) and can
use a consistent description for how it is passed.

A secondary benefit is that it allows multiline labels and avoid
workaround like joining with `r'\n'`.

A final benefit is that it saves some unnecessary String formatting
since the callback isn't called if no expectations fail on the Subject,
or when used as a soft check where the failure details are ignored.

- Make the `label` arguments to `nest` and `nestAsync`, and the _label
  field in `_TestContext` an `Iterable<String> Function()`.
- Wrap strings that had been passed to `String` arguments with callbacks
  that return the string in a list.
- When writing the label in a failure, write all lines, and use a
  postfix " that:".
- Update some `Map` expectations which had manually joined with literal
  slash-n to keep the label or clause to a single line to take advantage
  of the multiline allowance. Split tests for the changed
  implementations and add tests for the descriptions with multiline
  examples. Some of these could have used multiline clauses before.
This is the main point of interaction for authors of extensions, so docs
cover a wide range of information, including how the descriptions will
be used in failure output.
@natebosch natebosch marked this pull request as ready for review February 3, 2023 05:38
@natebosch natebosch requested review from jakemac53 and lrhn February 3, 2023 05:38
Copy link
Member

@lrhn lrhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of useful information, but I'd prefer some more conceptual stuff up-front, to set the stage for the technical details below. (Introduce concepts before referring to them, so that I know the what and why about them.)

///
/// This is the surface of interaction for expectation extension method
/// implementations.
/// Expectation extension methods use [ContextExtension] to get access to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I start reading here (which I did), I don't know what an "expectation extension method" is. I assume it's an extension method.

Is there some overview documentation (probably elsewhere) which describes the general concepts and their relations, which can be linked to before going into details?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some overview documentation (probably elsewhere) which describes the general concepts and their relations, which can be linked to before going into details?

Not currently. Where do you think the best place to put that would be? The README?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That, and/or the library documentation for the public libraries.

And for each important concept, describe itself thoroughly in its documentation, and then mention and link to the other relevant concepts, enough to show their relation.

///
/// This is the surface of interaction for expectation extension method
/// implementations.
/// Expectation extension methods use [ContextExtension] to get access to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a technical comment. It's too early for that in a comment describing such a fundamental concept.
Start out describing the role and purpose at a higher abstraction level before diving into technical details.
(Then I'll understand why the technical details are there and why I should care. 😁)

I'd order the following sections in order of introducing concepts before referencing them, and by most-useful/most-often-useful first.

Start by determining who the documentation is for. My guess is that a Context is a concept must important to people writing new expectation extension methods, not for people simply using the checks package to write tests. (That's why it's exported from a separate context.dart library.)
So, write towards those, people who need to understand how and when to use a Context.

The order of concepts, as I currently understand things, could be:

  • (What:) A context represents an abstract value to be tested, with enough information to

    • Describe how the value was obtained and
    • perform some test-related operations on the value.
  • There may or may not be an actual value, depending on, for example,
    whether earlier operations succeeded or not.

  • (Why:) A [Context] is the underlying primitive used to write type-specific user-aimed expectation extension methods like the extension method [CoreChecks.equals] on [Subject].
    The context provides an interface to the underlying test framework to report failures,
    and generate readable descriptions for a failure.

  • (Relation to more known concept:) A [Subject] is a wrapper around a Context which hides the Context methods,
    so that a [Subject] can freely have extension members of any name.
    The [ContextExtension] extension allows accessing the [Context] of a [Subject]
    when writing your own expectation extension methods.

  • (How to use:) There are two kinds of operations on a value that are exposed by a context:

    • Extracting new values from the current value.
    • Expecting something of the current value.
  • Both kinds of operation will be recorded and performed if/when an actual value is available.

  • Both kinds of operations also provide description callbacks that can be used
    to give a textual representation of what is happening.

  • (And if no value can be supplied, it's possible to react to that too?)

  • Extracting a new value from the current value produces new context for the new value.

    • Use [nest] or [nestAsync] to describe how to extract a value.
    • The new context has the current context as "parent" context,
      and the chain of contexts describe the operations used to get
      to the value.
    • Has a label description callback to describe how the value is extracted.
      It is called only when necessary.
  • Expecting something of a value records the expectation.

    • Use [expect], [expectAsync] or [expectUnawaited] methods to add an expectation.
    • Each expectation provides a predicate which can perform a test on an
      actual value, and either report a [Rejection] or not, in a way which depends
      on which method is used.
    • Has a clause description callback to create a textual description of the test being performed. It's called only when necessary.
      • <description of clause here>
      • Put "Where possible, string formatting for the descriptions should be performed in the callback, not ahead of time." here
  • (Behavior:) The actual behavior when a value is available is:

    • Extraction operations are performed, and the resulting value is provided
      to the corresponding nested context.
      • (If that throws, it's considered a test failure?)
    • Expectations' predicates are tested against the value.
    • If they result in a rejection, the expectations fails.
      • <Section on expectation failing>
    • Otherwise, if all expectations succeed, <section on all expectations succeeding>.

I think most of the behavior-information is here, a lot of the rest too, but the order doesn't let me understand underlying concepts before I see technical details about them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super helpful, and I agree it would be more clear to reorder the information.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I didn't use the exact order you suggested, but I used this structure for inspiration and I do think the doc is easier to read through now.

WDYT?

/// used and the callbacks will not be called. Where possible, string formatting
/// for the descriptions should be performed in the callback, not ahead of time.
///
/// When an expectation is used on a [ConditionSubject] and passed to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a [ConditionSubject] and is it important here?
(Aka, what is its relation to Context?)

Should this just be documented on the ConditionSubject class itself? (Probably.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rephrased this so it's hopefully more clear.

/// implementations.
/// Expectation extension methods use [ContextExtension] to get access to the
/// subject's [Context]. Expectations pass callbacks which describe what is
/// checked, and perform the expectation, respectively.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Expectations pass callbacks" ...

I read "expectations" as the abstract concept, not concrete code.

If so, expectations do not "pass callbacks". Checking an expectation might, because checking an expectation is a runtime operation, which can have the runtime behavior of passing callbacks.

(Also, needs defining the concept of "description callback".)

In general, be very careful about the use of the word "expectations".
It's generally used as an abstract conceptual thing, but it's easy to mix it with "expectation extension methods" (code concept) and the internal representation of an expectation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I fixed it so that I consistently use "expectation extension method" (or in a few places "expectation extension"), and the term "expectation" should only be used in the more general "a thing that can be expected of a subject" sense.

In the user facing docs describing writing tests, I suspect it will feel natural to describe calling an expectation extension method on a subject as "checking an expectation" on the subject, or for a ConditionSubject "recording and expectation".

/// description callbacks are used.
///
/// When both callbacks are used, the description callback will always be called
/// strictly after the expectation callback is called.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this important? (That is: Do you have, or expect, code which depends on that order? If not, it might be safer to not make the promise. Such code would likely maintain internal state which makes it impossible to replay the same check/description on a separate value, which would make me very disappointed. I still want to do myIntegers.check.each > 0;, which means replaying the expectation, likely using a ConditionalSubject, on every element of the iterable.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have, or expect, code which depends on that order?

Yes. Sometimes where I can't do async work for the description callback, I might have a fallback description by default, but gather some async information during the predicate and format it for a richer description when possible.

For example the inOrder extension method cannot synchronously describe the Condition instances it will check because they will use async expectations. It has a fallback for describe calling out the number of conditions that need to be satisfied, but only shows their details when the actual has run.

await _expectAsync(
() => descriptions.isEmpty
? ['satisfies ${conditions.length} conditions in order']
: descriptions, (actual) async {

Such code would likely maintain internal state which makes it impossible to replay the same check/description on a separate value, which would make me very disappointed.

That is the case. We should investigate whether there is a different way to provide a good UX for this.
Maybe we can devise an explicit way for the expectation callback to communicate to the description callback that would be compatible with reuse. Or maybe we should sacrifice the UX for those use cases.

Add a doc on the `_label` member.

Add doc on `_child` constructor.
Expand description of label a bit. Remove reference to "single line"

A future PR will make the line-per-element behavior more clear at the
class level.
Base automatically changed from label-callback to master February 4, 2023 01:56
@natebosch natebosch requested a review from lrhn February 4, 2023 06:32
They don't need to be read externally, the callers only care about the
two constructors, and these details are good to hide because the two
nullable field pattern is ugly.

This also fixes dartdoc references to the constructors with
[Extracted.rejection]. Otherwise it would link to the field.
Copy link
Contributor

@jakemac53 jakemac53 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I think my main comment would be that there is no example code in these docs, or links to examples.

I think a lot of people learn best by example, and just linking to existing (built-in) extensions from several of the sections, or providing really basic examples inline, would help a lot.

///
///
/// Whichever type of operation, an expectation extension method provides two
/// callbacks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some sort of link to the api here would be good, to show how these are provided, it's a bit confusing without that imo. I end up wanting to interrupt by reading to find that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded this to show one callback signature, and describe that the other signature varies. Does that help?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am confused by here isn't what the callbacks are supposed to do, its how/where they are provided.

I think this stems from the mentions of expect etc being below this - these are describing how those work right? Feels like its putting the cart before the horse a bit, I don't have the context yet for what is being described.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about mentioning each method in the bullet list above of the two types of operations?

@natebosch natebosch requested a review from jakemac53 February 6, 2023 18:46
@natebosch
Copy link
Member Author

or providing really basic examples inline

I was a little worried about the docs getting too long. I also don't know how much we want to repeat between the README and the API docs - maybe we should be OK with copy pasting a bunch between them?

Would it work to add an example usage similar to what is in the README on each of the individual APIs, and leave the class level doc as is?

@jakemac53
Copy link
Contributor

I was a little worried about the docs getting too long. I also don't know how much we want to repeat between the README and the API docs - maybe we should be OK with copy pasting a bunch between them?

Would it work to add an example usage similar to what is in the README on each of the individual APIs, and leave the class level doc as is?

I think copy/pasting between them is fine. I do think the class level doc here should have examples, or link to examples.

@natebosch
Copy link
Member Author

I do think the class level doc here should have examples, or link to examples.

I added a couple sync examples, one of expect and one of nest. It is towards the end of the doc. I added examples of all operations to the respective docs.

I also moved the detail about when callbacks when/won't be called towards the end of the doc. It's probably a detail most folks won't care about - it motivates the design but isn't important to know to use the design. It also now follows another mention of softCheck which I think makes it less confusing.

Had done a tweak in `has` that I noticed when copy/pasting it as an
example, but then I decided to go with creating fake examples for all
the operations because we don't have any nice small example for
`expectAsync`.
@natebosch natebosch merged commit f2d97bf into master Feb 6, 2023
@natebosch natebosch deleted the label-callback--context-docs branch February 6, 2023 22:02
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

Successfully merging this pull request may close these issues.

3 participants