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

No type safety around test signatures #351

Open
Jameskmonger opened this issue Apr 10, 2017 · 20 comments
Open

No type safety around test signatures #351

Jameskmonger opened this issue Apr 10, 2017 · 20 comments

Comments

@Jameskmonger
Copy link
Contributor

Currently, these two compile fine:

@Test()
public someTestWithParams(someParam: any, otherParam: any) { }

@TestCase(1, 2, 3)
public someTestWithoutParams() { }

It would be good to get compile-time safety around these.

@jamesadarich
Copy link
Member

Hey, @Jameskmonger not sure we could get much of use at compile time for this but worth an investigation :)

Maybe the better solution is pretest warning messages?

@jamesadarich
Copy link
Member

What do you think of the suggestion above @Jameskmonger?

@Jameskmonger
Copy link
Contributor Author

I have found a solution to this @jamesrichford but it will need to go into 2.0.0 as it will be breaking change (anyone with badly typed tests will break).

@Jameskmonger
Copy link
Contributor Author

See microsoft/TypeScript#1024 and http://stackoverflow.com/questions/43668884/get-signature-of-method-from-method-decorator

Another solution would be to change signature to this:

@TestCase([1, 2, null], "Some test case giving 1, 2, null")

This would be much easier to do.

@jamesadarich
Copy link
Member

@Jameskmonger looks good. What about for tests without test cases?

@jamesadarich jamesadarich added this to the 2.0.0 milestone Apr 29, 2017
@Jameskmonger
Copy link
Contributor Author

@Test("Some test without arguments")

@jamesadarich
Copy link
Member

@Jameskmonger I think you misunderstood. How would you make it type safe.

We need to keep the description separate otherwise you'll have to repeat it for every test case or have inconsistent decorators.

So given this, how will we say how many arguments the function should take if it doesn't have the TestCase decorator?

@Jameskmonger
Copy link
Contributor Author

Jameskmonger commented Apr 29, 2017

Ah sorry yeah I understand. In hindsight I don't think we'll be able to get type safety around the Test decorator in terms of ensuring that it has no parameters.

Of course, we could always roll them into one and have a separate TestDescription decorator?

@TestDescription("Test without parameters")
@Test()
public shouldDoSomething() { }

@TestDescription("Test with parameters")
@Test(1, 3)
@Test(5, 10)
public shouldDoSomething(a: number, b: number) { }

This way we can ensure that test parameters <-> signature parameters always match up

@Jameskmonger
Copy link
Contributor Author

Anyway, here is my implementation for TestCase:

interface TestCaseDecoratorImplementation<TProperty> {
  (
    target: object,
    propertyKey: string,
    descriptor?: TypedPropertyDescriptor<TProperty>
  ): void;
}

const TestCaseDecorator = <TArgs, TProperty>(args: TArgs): TestCaseDecoratorImplementation<TProperty> => {
  return (
    target: object,
    propertyKey: string,
    descriptor?: TypedPropertyDescriptor<TProperty>
  ) => { }
}

function TestCase<T1, T2>(args: [T1, T2]): TestCaseDecoratorImplementation<(arg1: T1, arg2: T2) => any>;
function TestCase<T1>(args: [T1]): TestCaseDecoratorImplementation<(arg1: T1) => any>;
function TestCase(args: Array<any>) {
  return TestCaseDecorator(args);
}

class SomeClass {
  @TestCase([ "cool value" ]) // ok
  @TestCase([ 3 ]) // not ok
  public stringTestCase(val: string) {}

  @TestCase([ "ok", { val: 5 } ])
  @TestCase([ false, false ]) // not ok
  public twoValTestCase(first: any, second: { val: number }) {}
}

@Jameskmonger
Copy link
Contributor Author

By making args optional and returning TestCaseDecoratorImplementation<() => any>, we can enforce type safety around the Test decorator (after rolling them into one)

function Test<T1, T2>(args: [T1, T2]): TestCaseDecoratorImplementation<(arg1: T1, arg2: T2) => any>;
function Test<T1>(args: [T1]): TestCaseDecoratorImplementation<(arg1: T1) => any>;
function Test(args?: Array<any>): TestCaseDecoratorImplementation<() => any> {
  return TestCaseDecorator(args);
}

class SomeClass {
  @Test([ "cool value" ]) // ok
  @Test([ 3 ]) // not ok
  public stringTestCase(val: string) {}

  @Test([ "ok", { val: 5 } ])
  @Test([false, false]) // not ok
  @Test()
  public twoValTestCase(first: any, second: { val: number }) { }

  @Test(5, 10) // not ok
  public noValTestCase() { }
}

@jamesadarich
Copy link
Member

Probably even better might be to remove the array from the overloads and use the spread operator on the main function :)

@Jameskmonger
Copy link
Contributor Author

You can't type a spread operator from what I've seen @jamesrichford

@jamesadarich
Copy link
Member

I'll show you what I mean either tomorrow or Monday as I'm away from my PC just now @Jameskmonger

@jamesadarich
Copy link
Member

@Jameskmonger

interface TestCaseDecoratorImplementation<TProperty> {
  (
    target: object,
    propertyKey: string,
    descriptor?: TypedPropertyDescriptor<TProperty>
  ): void;
}

const TestCaseDecorator = <TArgs, TProperty>(args: TArgs): TestCaseDecoratorImplementation<TProperty> => {
  return (
    target: object,
    propertyKey: string,
    descriptor?: TypedPropertyDescriptor<TProperty>
  ) => { }
}

function Test<T1, T2>(arg1: T1, arg2: T2): TestCaseDecoratorImplementation<(arg1: T1, arg2: T2) => any>;
function Test<T1>(arg1: T1): TestCaseDecoratorImplementation<(arg1: T1) => any>;
function Test(): TestCaseDecoratorImplementation<() => any>;
function Test(...args: Array<any>) {
  return TestCaseDecorator(args);
}

class SomeClass {
  @Test("cool value") // ok
  @Test(3) // not ok
  public stringTestCase(val: string) {}

  @Test("ok", { val: 5 })
  @Test(false, false) // not ok
  public twoValTestCase(first: any, second: { val: number }) {}

  @Test(5, 10) // ok?
  public noValTestCase() { }
}

this is roughly what I meant but interestingly looks like your suggestion for empty doesn't flag up an issue with the last scenario

@jamesadarich
Copy link
Member

@Jameskmonger it looks like this declaration doesn't care about extra arguments which is causing the issue here because also the below is OK by the compiler

  @Test("cool value", 42) // ok?  
  public stringTestCase(val: string) {}

@Jameskmonger
Copy link
Contributor Author

That's expected behaviour @jamesrichford - the rest parameter allows for 0 children:

function Foo(...args: Array<any>) {
	return 0;
}

function Bar(args: Array<any>) {
	return 1;
}

Foo(); // ok
Bar(); // Supplied parameters do not match any signature of call target.

@jamesadarich
Copy link
Member

@Jameskmonger the problem isn't the 0 children thing it's the extra children that are allowed. This isn't actually to do with the spread operator as we can see this in two other scenarios

function Test(): TestCaseDecoratorImplementation<() => any>;
function Test<T1>(args: [ T1 ]): TestCaseDecoratorImplementation<(arg1: T1) => any>;
function Test<T1, T2>(args: [ T1, T2 ]): TestCaseDecoratorImplementation<(arg1: T1, arg2: T2) => any>;
function Test<T1, T2>(args?: Array<any>) {
  return TestCaseDecorator(arguments);
}

class SomeClass {
  @Test([ "cool value" ]) // ok
  @Test([ "cool value", 42 ]) // ok?
  @Test([ 3 ]) // not ok
  public stringTestCase(val: string) {}

  @Test([ "ok", { val: 5 } ]) // ok
  @Test([ false, false ]) // not ok
  public twoValTestCase(first: any, second: { val: number }) {}

  @Test([ 5 ]) // ok?
  @Test() // ok
  public noValTestCase() { 

  }
}
function Test(): TestCaseDecoratorImplementation<() => any>;
function Test<T1>(arg1: T1): TestCaseDecoratorImplementation<(arg1: T1) => any>;
function Test<T1, T2>(args: T1, arg2: T2): TestCaseDecoratorImplementation<(arg1: T1, arg2: T2) => any>;
function Test<T1, T2>(arg1?: T1, arg2?: T2) {
  return TestCaseDecorator(arguments);
}

class SomeClass {
  @Test("cool value") // ok
  @Test("cool value", 42) // ok?
  @Test(3) // not ok
  public stringTestCase(val: string) {}

  @Test("ok", { val: 5 }) // ok
  @Test(false, false) // not ok
  public twoValTestCase(first: any, second: { val: number }) {}

  @Test(5) // ok?
  @Test() // ok
  public noValTestCase() { 

  }
}

Not had a chance to look at this a great deal but looks like on the face of it this may be a bug in TypeScript

@jamesadarich
Copy link
Member

@Jameskmonger for clarity the issues are marked with the comment // ok?

@jamesadarich
Copy link
Member

These compile where they shouldn't

@jamesadarich jamesadarich removed this from the 3.0.0 milestone Oct 19, 2018
@jamesadarich
Copy link
Member

The below is something like what we need to do but not possible as things stand (see microsoft/TypeScript#2607)

export function TestCase<Target extends { [PropertyKey in keyof Target]: Target[PropertyKey] },
PropertyKey extends keyof Target & string>(...testCaseArguments: Parameters<Target[PropertyKey]>) {
	return (
		target: Target,
		propertyKey: PropertyKey,
		descriptor?: TypedPropertyDescriptor<Target[PropertyKey]>
	) => {

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

No branches or pull requests

2 participants