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

I don't understand how to compose Intents/Effects together properly #84

Open
ckp95 opened this issue Sep 11, 2020 · 1 comment
Open

Comments

@ckp95
Copy link

ckp95 commented Sep 11, 2020

I'm trying to get my head around this library and I'm having some trouble understanding how I'm meant to structure Effects that are built out of other Effects, and how to test them. Let's say I have an Intent that represents getting text input from the user:

@dataclass
class GetInput:
    value: str = None

I write a @do-annotated generator function for it:

@do
def get_input(prompt):
    the_input = yield Effect(GetInput(prompt))
    return the_input

And I test it like this:

def test_get_input():
    prompt = "enter something: "
    seq = [
        (GetInput(prompt), lambda x: "The Quick Brown Fox")
    ]
    
    effect = get_input(prompt)
    assert perform_sequence(seq, effect) == "The Quick Brown Fox"

So far so good. I also want to have an Effect that represents printing something to the screen, so I write this:

@dataclass
class PrintString:
    value: str = None
    

@do
def print_string(value):
    yield Effect(PrintString(value))
    

def test_print_string():
    to_print = "something"
    seq = [
        (PrintString("something"), noop)
    ]
    
    effect = print_string(to_print)
    assert perform_sequence(seq, effect) is None

That works fine too.

If I want to combine these two, with some additional processing (converting to lowercase) I know I can do something like this:

@do
def print_lowercased_input(prompt):
    the_input = yield get_input(prompt)
    lowered = the_input.lower()
    yield print_string(lowered)


def test_print_lowercased_input():
    prompt = "enter something: "
    seq = [
        (GetInput(prompt), lambda x: "The Quick Brown Fox"),
        (PrintString("the quick brown fox"), noop)
    ]
    
    effect = print_lowercased_input(prompt)
    assert perform_sequence(seq, effect) is None

But how do I encapsulate these two Effects into one "thing", in a way that can be tested without having to know about the individual sub-Effects that comprise it? In other words, what I would like is to have this:

@dataclass
class PrintLowercasedUserInput:
    prompt: str = None

@do
def combined_effect(prompt):
    yield Effect(PrintLowercasedUserInput(prompt))
    

def test_combined_effect():
    prompt = "enter something: "
    seq = [
        (PrintLowercasedUserInput(prompt), lambda x: "the quick brown fox"),
    ]
    
    effect = combined_effect(prompt)
    assert perform_sequence(seq, effect) is None

In such a way that PrintLowercasedUserInput somehow encapsulates the action of the print_lowercased_input generator.

I'm finding it really hard to express what I want with the right vocabulary because this is all quite new to me. Basically when I'm writing tests for a large application I don't want the seq in the test to be full of very low level actions, I want to group them together into logical units, each of which has tests itself. Am I making any sense?

@radix
Copy link
Contributor

radix commented Sep 17, 2020

@ckp95 yeah, sure, that makes sense. There are two strategies I have used:

  1. I think the simplest and most practical is to write test utility functions that correspond to your encapsulated, multi-step effects, so your higher-level tests can construct effect sequences by concatenating together the results of these utility functions. e.g.:
def fox():
    return [
        (GetInput("enter something: "), lambda x: "The Quick Brown Fox"),
        (PrintString("the quick brown fox"), noop)
    ]

and then you would set your seq to seq = fox() + [...] when testing those effects in conjunction with other ones. (parameterize fox as necessary).

  1. The other way would be to do what you were hinting at in your example test, and actually create a new application-level Intent called PrintLowercasedUserInput which, in its performer, performs the other effects. You can then skip the sub-effects like your final example does. I generally don't use this pattern1

1 though I do use something like it when I need effects which "wrap" other effects, like this one which binds some fields into a logging context and then executes another effect: https://github.com/rackerlabs/otter/blob/eb2e0bde6b26badb0d6f5291bc501a641d4b4148/otter/log/intents.py#L62. Then, I test it with the effect.testing.nested_sequence -- however, this isn't really the same situation that you're asking about.

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

No branches or pull requests

2 participants