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

Make test context optionally type aware for TypeScript #1298

Merged
merged 21 commits into from
Mar 14, 2017
Merged

Make test context optionally type aware for TypeScript #1298

merged 21 commits into from
Mar 14, 2017

Conversation

mmkal
Copy link
Contributor

@mmkal mmkal commented Mar 5, 2017

This sets out to solve the problem highlighted in #1291

Usage example: see comment further down.

mmkal added 4 commits March 5, 2017 14:13
Also add 'contextualize' function to main module exports, which will allow seeding the context, and return a test function which will give the context property the correct type.
Copy link
Member

@novemberborn novemberborn left a comment

Choose a reason for hiding this comment

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

Clever!

Would it be possible to do:

function contextualize<T>(getContext: () => T): ava.ITest<T> {

(Without repeating the { context } bit?

Perhaps @despairblue, @ivogabe, @SamVerschueren could provide input as well? 😇

.gitignore Outdated
@@ -3,3 +3,6 @@ node_modules
coverage
bench/.results
types/generated.d.ts

# VS Code configuration
.vscode
Copy link
Member

Choose a reason for hiding this comment

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

You should add this to your machine's .gitignore, rather than adding it in this repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I didn't want to do that initially because I have other repos that I want .vscode files checked in for, but maybe I can use .git/info/exclude instead.

types/base.d.ts Outdated
export function test(run: ContextualTest): void;
export function test(name: string, run: Macros<ContextualTestContext>, ...args: any[]): void;
export function test(run: Macros<ContextualTestContext>, ...args: any[]): void;
export interface ITest<T> {
Copy link
Member

Choose a reason for hiding this comment

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

Let's just use Test<T>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can do - I think my C# is showing 😳

export type Test = (t: TestContext) => PromiseLike<void> | Iterator<any> | Observable | void;
export type ContextualTest = (t: ContextualTestContext) => PromiseLike<void> | Iterator<any> | Observable | void;
export type GenericTest<T> = (t: GenericTestContext<T>) => PromiseLike<void> | Iterator<any> | Observable | void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Add export type ContextualTest = GenericTest<{ context: any }>;, otherwise this might break some users. Same for ContextualCallbackTest

@ivogabe
Copy link
Contributor

ivogabe commented Mar 7, 2017

Overall this looks good. Could you add some comments to make.js? It might be a bit hard to read now.

@mmkal
Copy link
Contributor Author

mmkal commented Mar 7, 2017

@novemberborn re whether we could do:

function contextualize<T>(getContext: () => T): ava.ITest<T> { ... }

Maybe what I could do is export two types - Test<T> (renamed from ITest) and GenericContextualTest<T> which could be defined by

export type GenericContextualTest<T> = Test<{ context: T }>;

The name GenericContextualTest isn't great, but I'll see what I can do about avoiding a clash with ContextualTest when implementing @ivogabe's suggestion.

Anyway, this way people wouldn't have to constantly repeat the context: ... part. I'll add something along those lines soon and update.

@mmkal
Copy link
Contributor Author

mmkal commented Mar 8, 2017

@novemberborn @ivogabe updated with suggested changes, and improved a couple of other things:
I renamed ITest as suggested, but I am now using it as the interface for all test-like functions - and they are all now optionally generic. Plus, types/generated.d.ts is now about 25% smaller because each test-like function now implements this single interface rather than having its declaration spread over four lines.

Usage example:

import * as ava from 'ava'

function contextualize<T>(getContext: () => T): ava.ContextualTestFunction<T> {
    ava.test.beforeEach(t => {
        Object.assign(t.context, getContext());
    });

    return ava.test;
}

const test = contextualize(() => ({ foo: 'bar' }));

test.beforeEach(t => {
    t.context.foo = 123; // error:  Type '123' is not assignable to type 'string'
});

test.after.always.failing.cb.serial('very long chains are properly typed', t => {
    t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type '{ foo: string }'
});

test('an actual test', t => {
    t.deepEqual(t.context.foo.map(c => c), ['b', 'a', 'r']); // error: Property 'map' does not exist on type 'string'
});

In the above example, contextualize is a generic method, and infers the type of the object passed to it to be { foo: string }. By using the test method returned instead of the vanilla ava one, we can take advantage of type annotations.

This would allow people to extend ava with things like their favourite assertion/mocking libraries, and have strongly typed, convenient test setups:

import MyClass from '../MyClass';
import calc = require('npm-calculator');
import chai = require('chai');
import sinon = require('sinon');
import contextualize from './ava-contextualize'; // same contextualize method as above as a module

const test = contextualize(() => {
    const myClass = new MyClass();
    const sandbox = sinon.sandbox.create();
    const expect = chai.expect;
    return { myClass, sandbox, expect };
});

test.afterEach(t => t.sandbox.restore());

test('times uses npm-calculator multiply', t => {
    t.context.sandbox.stub(calc, 'multiply').returns(42);
    const sixTimesNine = t.context.myClass.times(6, 9);
    t.context.expect(calc.multiply).to.be.calledOnce;
    t.context.expect(sixTimesNine).to.equal(42);
});

@mmkal mmkal changed the title [WIP] Make test context (optionally) type aware. Make test context (optionally) type aware. Mar 8, 2017
@mmkal
Copy link
Contributor Author

mmkal commented Mar 9, 2017

Here's a gist of the generated types file, since it can't be seen on the PR.
@novemberborn @despairblue @ivogabe I'd love to get this merged as I'm planning to use it in my current project. Sorry about so many small commits - I'd be happy for it to go in as it is now, if it looks OK to you.

Copy link
Contributor

@ivogabe ivogabe left a comment

Choose a reason for hiding this comment

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

Thanks for the gist, the generated file looks good to me. Using underscores is not very common, but is probably the best way to go. With caps we could have some problems, for instance testAfterEach could then be after.each and afterEach. Though I think that we should rename the first part of the names, since types should start with an upper case letter. So maybe one of these:

  • Rename TestFunction(Core) to Register(Base), and test_** to Register_**
  • Rename TestFunction(Core) to AVA(Core), and test_** to AVA_.

types/make.js Outdated
// Returns the declarations for the 'test' function and core types
function testFunctionDeclarations() {
return (
`export default test;
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this to base.d.ts

Copy link
Contributor Author

@mmkal mmkal Mar 9, 2017

Choose a reason for hiding this comment

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

If I do, base.d.ts will no longer compile because ContextualTestFunction and TestFunction (or Register and RegisterContextual once they're renamed) don't exist in base.d.ts.

Or do you think that doesn't matter so much because base.d.ts isn't used for anything except seeding generated.d.ts?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's not an issue, base.d.ts is indeed only used by the generation script to write generated.d.ts.

types/base.d.ts Outdated
export type ContextualCallbackTest = (t: ContextualCallbackTestContext) => void;
export type GenericCallbackTest<T> = (t: GenericCallbackTestContext<T>) => void;

interface Context<T> { context: T }
Copy link
Contributor

Choose a reason for hiding this comment

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

Add export

types/base.d.ts Outdated
export function test(run: ContextualTest): void;
export function test(name: string, run: Macros<ContextualTestContext>, ...args: any[]): void;
export function test(run: Macros<ContextualTestContext>, ...args: any[]): void;
interface TestFunctionCore<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we think of a better name here? Technically this is not the function that does the tests, it only registers a test. So maybe something like RegisterBase<T>, and rename TestFunction Register<T>?

And add export here

Also rename test_* interfaces to Register_*.
@mmkal
Copy link
Contributor Author

mmkal commented Mar 10, 2017

@ivogabe updated with the exports and renames you suggested. Gist also updated.

@sindresorhus
Copy link
Member

This should be documented somewhere, probably here: https://github.com/avajs/ava/blob/master/docs/recipes/typescript.md

@mmkal
Copy link
Contributor Author

mmkal commented Mar 11, 2017

@sindresorhus added the above usage example.

@ivogabe
Copy link
Contributor

ivogabe commented Mar 11, 2017

Looks good to me, I've tested it on one of my projects and it worked just fine.

This does however not work with TypeScript 2.0, 2.1 is required (because of the use of T['skip']). 2.1 was released december last year. But that's probably not an issue since AVA is not 1.0 yet.

readme.md Outdated
@@ -578,6 +578,8 @@ Keep in mind that the `beforeEach` and `afterEach` hooks run just before and aft

Remember that AVA runs each test file in its own process. You may not have to clean up global state in a `after`-hook since that's only called right before the process exits.

### Test context
Copy link
Member

Choose a reason for hiding this comment

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

### => ####

@sindresorhus sindresorhus changed the title Make test context (optionally) type aware. Make test context optionally type aware Mar 12, 2017
@sindresorhus sindresorhus changed the title Make test context optionally type aware Make test context optionally type aware for TypeScript Mar 12, 2017
@sindresorhus
Copy link
Member

Thank you @mmkal. This is a great improvement! :)

@sindresorhus sindresorhus merged commit 50ad213 into avajs:master Mar 14, 2017
@mmkal
Copy link
Contributor Author

mmkal commented Mar 14, 2017

Thanks @sindresorhus @novemberborn @ivogabe ! Does it get auto-published to NPM? Or is it done ad-hoc?

@sindresorhus
Copy link
Member

@mmkal No, releases are manually published. Was planning to do a new one next week (It's a lot of work to manually test and write release notes).

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.

4 participants