Ideas on a testing focussed code transform

Isiah Meadows isiahmeadows at gmail.com
Sat Dec 16 17:07:42 UTC 2017


You're still changing code as evidenced by your original email.

- With dependency injection, you're changing the call sites (name -> method
call) and argument count or state size (to hold the injection).
- With your idea, you're changing the call sites (name+args to `yield`) and
return value (value -> `yield` result).

To give you an idea what I mean by dependency injection, take a look at
this:

- `util` uses here:
https://github.com/isiahmeadows/thallium/blob/master/lib/cli/run.js
- `state.util` uses here:
https://github.com/isiahmeadows/thallium/blob/master/lib/cli/loader.js
- Main `state.util` definition:
https://github.com/isiahmeadows/thallium/blob/master/lib/cli/util.js
- Testing `state.util` definition (`Mock` class):
https://github.com/isiahmeadows/thallium/blob/master/test-util/cli/cli.js

I mean small functional dependency injection, not the boilerplatey
Java-style one. Mine is a bit more complex, because I also am passing
shared state with it (like parsed args and config), but it's just an extra
object property on that state, as defined in the `State` constructor in
`lib/cli/run.js`.

On Sat, Dec 16, 2017, 11:51 Pranay Prakash <pranay.gp at gmail.com> wrote:

> I don't understand what you mean by "record actions done by iterables".
> Could you please expand on that :)
>
> My main reason for being weary of dependency injection is that I don't
> want to change how I write application code just to make it more testable.
> That's a pretty well shared concern about dependency injection whereby it
> affects the entire structure of your codebase, or adds additional arguments
> to simple functions for the sole purpose of testing it.
>
> With the method I'm proposing, you wouldn't have to change how you code at
> all, and I think that's really nice
>
> On Sat, 16 Dec 2017, 10:32 Isiah Meadows, <isiahmeadows at gmail.com> wrote:
>
>> I think what you're looking for is dependency injection, which is simply
>> the functions you care about passed as an argument rather than sent from
>> `yield`. In your case, the injections would be recording what method is
>> called when and with what, but that's what I typically do. As an added
>> bonus, I don't need to rely on how the functions are structured - I can
>> even record actions done from iterables. (That's one area yours won't work.)
>>
>> The only catch is that you can't asynchronously block, but that's far
>> more rare in practice (automated testing doesn't need it, and you can set
>> breakpoints when debugging, removing the need to asynchronously hook into
>> it).
>>
>> On Sat, Dec 16, 2017, 00:19 Pranay Prakash <pranay.gp at gmail.com> wrote:
>>
>>> Hmm, I suppose the stack traces drawback doesn't matter as long you're
>>> only using this within the context of testing. The emphasis here is that
>>> the actual application code that you run in production is what you wrote
>>> (the imperative function calls), but when you 'require' the function into
>>> your testing code, it gets transformed into this pure generator function
>>> that's easier to test.
>>>
>>> I don't see how the state/future of async functions affects this. In
>>> fact, in my example, I use an async function to try and show that this
>>> transform can even work for async functions (and will convert them to a
>>> synchronous generator function rather easily)
>>>
>>> On Fri, 15 Dec 2017 at 23:14 Isiah Meadows <isiahmeadows at gmail.com>
>>> wrote:
>>>
>>>> You're describing a variant of the interpreter pattern. I've used
>>>> similar in task scheduling contexts, and the key drawback is you no longer
>>>> have useful stack traces.
>>>>
>>>> I haven't done any formal research on the approach, though. I will
>>>> caution you that async functions are already widely used enough
>>>> (particularly in Node) they're unlikely to go anywhere beyond something
>>>> drastic.
>>>>
>>>> On Sat, Dec 16, 2017, 00:07 Pranay Prakash <pranay.gp at gmail.com> wrote:
>>>>
>>>>> Hey all,
>>>>>
>>>>> I was just reading the docs for `redux-saga` where I encountered a
>>>>> nice design pattern for a saga which is (correct me if I'm wrong) a
>>>>> regular javascript generator function that yields the intent to call a
>>>>> function (instead of actually calling a function) until it goes through all
>>>>> the steps. If that doesn't make sense, consider this simple function:
>>>>>
>>>>> ```
>>>>> async function findFriends() {
>>>>>   const myId = getMyID();
>>>>>   const myUser = await fetchUser(myId);
>>>>>   return myUser.friends;
>>>>> }
>>>>> ```
>>>>>
>>>>> instead of actually making the function calls needed, we can instead
>>>>> have a function that does something like this:
>>>>>
>>>>>
>>>>> ```
>>>>> function* findFriends() {
>>>>>   const myId = yield { fn: getMyID };
>>>>>   const myUser = yield { fn: fetchUser, args: [myId] };
>>>>>   return myUser.friends;
>>>>> }
>>>>> ```
>>>>>
>>>>> This is a pure generator function that doesn't actually do anything,
>>>>> but has all the necessary information to recreate the original function[1]
>>>>> (or have a library "trampoline" through the function and make all the
>>>>> necessary calls for you)
>>>>>
>>>>> A HUGE plus of the second version of the function is that it's
>>>>> *easily* testable (unlike the first one). Pure functions are easier to
>>>>> test. Testing this is simply a matter of calling `.next()`, getting the
>>>>> "intent", making sure the right intent was yielded (good enough for unit
>>>>> testing) and calling `.next()` again with the "mock" value, you want to
>>>>> return and continue to test.
>>>>>
>>>>> An observation to make here is that you can transform the original
>>>>> version of this code (easy, normal code to write) to the latter (easy code
>>>>> to test). So, what about having some sort of babel transform perhaps that
>>>>> can convert the first to the second but only in the context of unit tests.
>>>>> You write code the normal way as you would for your application and don't
>>>>> worry about test suite implementation details (mocking/dependency
>>>>> injection/etc.), and when you want to test, simply import your function
>>>>> (which gets converted to a generator) and step through it to test different
>>>>> scenarios.
>>>>>
>>>>> I personally think this is a super clean way to do testing since
>>>>> the tests never interfere with how you actually write the code AND you
>>>>> don't have to explicitly mock.
>>>>>
>>>>> Does anyone have thoughts on this / prior research (or knows about an
>>>>> existing implementation of this)?
>>>>>
>>>>> Cheers,
>>>>> Pranay
>>>>>
>>>> _______________________________________________
>>>>> es-discuss mailing list
>>>>> es-discuss at mozilla.org
>>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>>
>>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20171216/23d757bc/attachment.html>


More information about the es-discuss mailing list