最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Testing pure function on union type which delegates to other pure functions - Stack Overflow

programmeradmin4浏览0评论

Suppose you have function which takes a union type and then narrows the type and delegate to one of two other pure functions.

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}

Assume that fnForString() and fnForNumber() are also pure functions, and they have already themselves been tested.

How should one go about testing foo()?

  • Should you treat the fact that it delegates to fnForString() and fnForNumber() as an implementation detail, and essentially duplicate the tests for each of them when writing the tests for foo()? Is this repetition acceptable?
  • Should you write tests which "know" that foo() delegate to fnForString() and fnForNumber() e.g. by mocking them out and checking that it delegates to them?

Suppose you have function which takes a union type and then narrows the type and delegate to one of two other pure functions.

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}

Assume that fnForString() and fnForNumber() are also pure functions, and they have already themselves been tested.

How should one go about testing foo()?

  • Should you treat the fact that it delegates to fnForString() and fnForNumber() as an implementation detail, and essentially duplicate the tests for each of them when writing the tests for foo()? Is this repetition acceptable?
  • Should you write tests which "know" that foo() delegate to fnForString() and fnForNumber() e.g. by mocking them out and checking that it delegates to them?
Share Improve this question asked Oct 11, 2019 at 8:38 samfrancessamfrances 3,7673 gold badges28 silver badges48 bronze badges 2
  • 2 You created your own overloaded function with hard coded dependencies, Another way to achieve this kind of polymorphism (ad-hoc polymorphism to be precisely) is to pass the function dependencies as an argument (a type directory). Then you could use mock functions for testing purposes. – user5536315 Commented Oct 11, 2019 at 10:37
  • Ok, but that's more a case of "how to achieve mocking" - so yes, you could pass the functions in, or have a curried function, etc. But my question was more on the level of "how to mock?" but rather "is mocking the right approach in the context of pure functions?". – samfrances Commented Oct 11, 2019 at 15:50
Add a ment  | 

5 Answers 5

Reset to default 5

The best solution would be just testing for foo.

fnForString and fnForNumber are an implementation detail that you may change in the future without necessarily changing the behaviour of foo. If that happens your tests may break with no reason, this kind of problem makes your test too expansive and useless.

Your interface just needs foo, just test for it.

If you have to test for fnForString and fnForNumber keep this kind of test apart from your public interface tests.

This is my interpretation of the following principle stated by Kent Beck

Programmer tests should be sensitive to behaviour changes and insensitive to structure changes. If the program’s behavior is stable from an observer’s perspective, no tests should change.

Short answer: the specification of a function determines the manner in which it should be tested.

Long answer:

Testing = using a set of test cases (hopefully representative of all cases that may be encountered) to verify that an implementation meets its specification.

In the example foo is stated without specification, so one should go about testing foo by doing nothing at all (or at most some silly tests to verify the implicit requirement that "foo terminates in one way or another").

If the specification is something operational like "this function returns the result of applying args to either fnForString or fnForNumber according to the type of args" then mocking the delegates (option 2) is the way to go. No matter what happens to fnForString/Number, foo remains in accordance with its specification.

If the specification does not depend on fnForType in such a manner then re-using the tests for fnFortype (option 1) is the way to go (assuming those tests are good).

Note that operational specifications remove much of the usual freedom to replace one implementation by another (one that is more elegant/readable/efficient/etc). They should only be used after careful consideration.

In an ideal world, you would write proofs instead of tests. For example, consider the following functions.

const negate = (x: number): number => -x;

const reverse = (x: string): string => x.split("").reverse().join("");

const transform = (x: number|string): number|string => {
  switch (typeof x) {
  case "number": return negate(x);
  case "string": return reverse(x);
  }
};

Say you want to prove that transform applied twice is idempotent, i.e. for all valid inputs x, transform(transform(x)) is equal to x. Well, you would first need to prove that negate and reverse applied twice are idempotent. Now, suppose that proving the idempotence of negate and reverse applied twice is trivial, i.e. the piler can figure it out. Thus, we have the following lemmas.

const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;

const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;

We can use these two lemmas to prove that transform is idempotent as follows.

const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
  switch (typeof x) {
  case "number": return negateNegateIdempotent(x);
  case "string": return reverseReverseIdempotent(x);
  }
};

There's a lot going on here, so let's break it down.

  1. Just as a|b is a union type and a&b is an intersection type, a≡b is an equality type.
  2. A value x of an equality type a≡b is a proof of the equality of a and b.
  3. If two values, a and b, are not equal then it's impossible to construct a value of type a≡b.
  4. The value refl, short for reflexivity, has the type a≡a. It's the trivial proof of a value being equal to itself.
  5. We used refl in the proof of negateNegateIdempotent and reverseReverseIdempotent. This is possible because the propositions are trivial enough for the piler to prove automatically.
  6. We use the negateNegateIdempotent and reverseReverseIdempotent lemmas to prove transformTransformIdempotent. This is an example of a non-trivial proof.

The advantage of writing proofs is that the piler verifies the proof. If the proof is incorrect, then the program fails to type check and the piler throws an error. Proofs are better than tests for two reasons. First, you don't have to create test data. It's difficult to create test data that handles all the edge cases. Second, you won't accidentally forget to test any edge cases. The piler will throw an error if you do.


Unfortunately, TypeScript doesn't have an equality type because it doesn't support dependent types, i.e. types that depend upon values. Hence, you can't write proofs in TypeScript. You can write proofs in dependently typed functional programming languages like Agda.

However, you can write propositions in TypeScript.

const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;

const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;

const transformTransformIdempotent = (x: number|string): boolean => {
  switch (typeof x) {
  case "number": return negateNegateIdempotent(x);
  case "string": return reverseReverseIdempotent(x);
  }
};

You can then use a library such as jsverify to automatically generate test data for multiple test cases.

const jsc = require("jsverify");

jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests

jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests

You can also call jsc.forall with "number | string" but I can't seem to get it to work.


So to answer your questions.

How should one go about testing foo()?

Functional programming encourages property-based testing. For example, I tested the negate, reverse, and transform functions applied twice for idempotence. If you follow property-based testing, then your proposition functions should be similar in structure to the functions that you're testing.

Should you treat the fact that it delegates to fnForString() and fnForNumber() as an implementation detail, and essentially duplicate the tests for each of them when writing the tests for foo()? Is this repetition acceptable?

Yes, is it acceptable. Although, you can entirely forego testing fnForString and fnForNumber because the tests for those are included in the tests for foo. However, for pleteness I would remend including all the tests even if it introduces redundancy.

Should you write tests which "know" that foo() delegate to fnForString() and fnForNumber() e.g. by mocking them out and checking that it delegates to them?

The propositions that you write in property-based testing follows the structure of the functions you're testing. Hence, they "know" about the dependencies by using the propositions of the other functions being tested. No need to mock them. You'd only need to mock things like network calls, file system calls, etc.

Assume that fnForString() and fnForNumber() are also pure functions, and they have already themselves been tested.

Well since implementation details are delegated to fnForString() and fnForNumber() for string and number respectively, testing it boils down to merely make sure that foo calls the right function. So yes, I would mock them and ensure that they are called accordingly.

foo("a string")
fnForNumberMock.hasNotBeenCalled()
fnForStringMock.hasBeenCalled()

Since fnForString() and fnForNumber() have been tested individually, you know that when you call foo(), it calls the right function and you know the function does what it is supposed to do.

foo should return something. You could return something from your mocks, each a different thing and ensure that foo returns correctly (for example, if you forgot a return in your foo function).

And all things have been covered.

I think it's useless to test the type of your function, the system can do this alone and allow you to give the same name to each of the types of objects that interest you

sample code

  //  fnForStringorNumber String Wrapper  
String.prototype.fnForStringorNumber = function() {
  return  this.repeat(3)
}
  //  fnForStringorNumber Number Wrapper  
Number.prototype.fnForStringorNumber = function() {
  return this *3
}

function foo( arg ) {
  return arg.fnForStringorNumber(4321)
}

console.log ( foo(1234) )       // 3702
console.log ( foo('abcd_') )   // abcd_abcd_abcd_

// or simply:
console.log ( (12).fnForStringorNumber() )     // 36
console.log ( 'xyz_'.fnForStringorNumber() )   // xyz_xyz_xyz_

I'm probably not a great theorist on coding techniques, but I did a lot of code maintenance. I think that one can really judge the effectiveness of a way of coding only on concrete cases, the speculation can not have value of proof.

发布评论

评论列表(0)

  1. 暂无评论