erikedin / Behavior.jl

Tool for Behavior Driven Development in Julia
Other
25 stars 3 forks source link

"@step" function decorator used in python #67

Closed mkschulze closed 3 years ago

mkschulze commented 3 years ago

Hi @erikedin,

I'm trying to write the integration tests from here currently, and they all start with a @step decorator in python.

How would you translate this to Julia using your package?

from behave import *

from tests.behaviour.context import Context

@step("connection has been opened")
def step_impl(context: Context):
    assert context.client and context.client.is_open()

@step("connection does not have any database")
def step_impl(context: Context):
    assert len(context.client.databases().all()) == 0
erikedin commented 3 years ago

Hi Mark!

The @step decorator would be replaced with either @given, @when, or @then. In actuality, they all expand to the same thing, so it doesn't matter which one you use for a step, but for readability I think it's best to separate them. The above steps would look like

using ExecutableSpecifications

@then "connection has been opened" begin
    @expect context[:cllient] !== nothing && is_open(context[:client])
end

@then "connection does not have any database" begin
    @expect length(get_all(databases(context[:client]))) == 0
end

Note that I'm guessing above about what the actual Grakn methods will look like in Julia, but that's how the steps look like anyway.

Also, be aware that there's a very good chance that this will change very slightly soon. So, above the context variable is magically in scope, which is because I put it in scope in the macros for @{given,when,then}. That is subject to change. It will instead probably look something, but not necessarily exactly like this (I'm not even sure the below code is correct)

using ExecutableSpecifications

@then "connection has been opened" context -> begin
    @expect context[:cllient] !== nothing && is_open(context[:client])
end

@then "connection does not have any database" context -> begin
    @expect length(get_all(databases(context[:client]))) == 0
end

So, instead of just having a begin/end block, the macro will instead take an anonymous function that will explicitly name context. I'll let you know if/when this happens though.

mkschulze commented 3 years ago

Ok, thank you. That helps!

mkschulze commented 3 years ago

the @expect macro is from your package and is the same as the @assert macro that can be found in Julia?

erikedin commented 3 years ago

@expect is from my package, yes. It's not exactly the same assert the @Assert macro, I think. I'm fairly sure I throw a custom exception from @expect. I need to make sure that @assert works well, too. It'll work, but if it fails, then the step will be listed as having had an unexpected exception, rather than failing.

mkschulze commented 3 years ago

ok, I'll use @expect then. Did you do the @expect as a safe way and independent of debugging settings? I read in python the execution of assert() might be disabled, according to the debug settings. Just curious.

erikedin commented 3 years ago

I genuinely don't remember exactly why, but that sounds reasonable. I think I initially looked at using the @test macro from the Test package, but I couldn't use it directly. I think I just made my own version of that.

tk3369 commented 3 years ago

Yeah, I think the use of @step seems to be a bad practice. The testing code looks better with @given, @when, and @then because the intention is more explicit.

mkschulze commented 3 years ago

Question is answered I think, closing now.

mkschulze commented 3 years ago

Reopening this to make sure I'm on the right track with the syntax.

from behave import *
from tests.behaviour.context import Context

@step("connection has been opened")
def step_impl(context: Context):
    assert context.client and context.client.is_open()

@step("connection does not have any database")
def step_impl(context: Context):
    assert len(context.client.databases().all()) == 0

I assume this would now look like

@given("connection has been opened") do context
    @fail context[:cllient] !== nothing && is_open(context[:client])
end

@given("connection does not have any database") do context
    @fail length(get_all(databases(context[:client]))) == 0
end

is this about right @erikedin?

erikedin commented 3 years ago

The assert macro ought to be @expect instead of @fail. The fail macro unconditionally fails the step. The @expect macro checks the conditions, which is what you want here. Other than that, it all looks good.

mkschulze commented 3 years ago

Ok, somehow the suggestmissingsteps function seems to always suggest a @fail statement. Good to know, I'll change it then, thx!

mkschulze commented 3 years ago

Can I ask another one? That would help me a lot I think.

There is this expression in python:

@step("{var:Var} = attribute({type_label}) as(boolean) put: {value:Bool}")
def step_impl(context: Context, var: str, type_label: str, value: bool):
    context.put(var, context.tx().concepts().get_attribute_type(type_label).as_remote(context.tx()).as_boolean().put(value))

so, the context .put method comes from an internal class here:

public class ThingSteps {

    private static Map<String, Thing> things = new HashMap<>();

    public static Thing get(String variable) {
        return things.get(variable);
    }

    public static void put(String variable, Thing thing) {
        things.put(variable, thing);
    }

from here: https://github.com/graknlabs/client-java/blob/master/test/behaviour/concept/thing/ThingSteps.java

What I've done now is this:

@when("{var:Var} = attribute({type_label}) as(boolean) put: {value:Bool}") do context
    @expect put(var::String, context[:tx(), :concepts(), :get_attribute_type(type_label::String), :as_remote(context[:tx()]), :as_boolean(), :put(value::bool)])
end

But it could be pretty wrong I fear.

erikedin commented 3 years ago

I'm not entirely sure, but I think they're using the put methods to place arbitrary objects in a specific place in the context, to share them between steps. This is something you could use the context for directly. The context is meant to carry objects from one step to another. I'm guessing, but I think they use the put method because Pythons behave doesn't let you set arbitrary fields based on what you get from the step, only hard-coded fields.

I don't know what tx().concepts().get_attribute_type(type_label).as_remote(context.tx()).as_boolean().put(value)) does, but it looks like they use this step to set some attribute on some Grakn transaction. This is something I probably can't help you with much, I'm afraid.

The part that does put on the context

context.put(var, ...) 

could easily be converted to Julia with

context[Symbol(var)] = ...

It's how I always intended context to be used. There's probably some reason the Grakn team added a put method on Behaves Context but I'm not sure it applies for ExecutableSpecifications, as you can already do this natively.

Also, you don't need the @expect macro here. The step appears to be used to perform some action and put some objects in the context for the next step to use. It doesn't assert anything, so @expect isn't needed.

mkschulze commented 3 years ago

ok, yes it's ment to set the background conditions of the scenarios here:

Feature: Concept Attribute

  Background:
    Given connection has been opened
    Given connection does not have any database
    Given connection create database: grakn
    Given connection open schema session for database: grakn
    Given session opens transaction of type: write
    # Write schema for the test scenarios
    Given put attribute type: is-alive, with value type: boolean # this is the one
    Given put attribute type: age, with value type: long
    Given put attribute type: score, with value type: double
    Given put attribute type: birth-date, with value type: datetime
    Given put attribute type: name, with value type: string
    Given put attribute type: email, with value type: string
    Given attribute(email) as(string) set regex: \S+@\S+\.\S+
    Given put entity type: person
    Given entity(person) set owns attribute type: is-alive
    Given entity(person) set owns attribute type: age
    Given entity(person) set owns attribute type: score
    Given entity(person) set owns attribute type: name
    Given entity(person) set owns attribute type: email
    Given entity(person) set owns attribute type: birth-date
    Given transaction commits
    Given connection close all sessions
    Given connection open data session for database: grakn
    Given session opens transaction of type: write

https://github.com/Humans-of-Julia/GraknClient.jl/blob/BDD--steps/test/behaviour/features/concept/thing/attribute.feature

This is how it's done in java

@When("{var} = attribute\\( ?{type_label} ?) as\\( ?boolean ?) put: {bool}")
    public void attribute_type_as_boolean_put(String var, String typeLabel, boolean value) {
        put(var, tx().concepts().getAttributeType(typeLabel).asBoolean().asRemote(tx()).put(value));
    }
mkschulze commented 3 years ago

Maybe like this then? What do you think @tk3369 ?

@when("{var:Var} = attribute({type_label}) as(boolean) put: {value:Bool}") do context
    context[Symbol(var)] = [:tx(), :concepts(), get_attribute_type(:type_label::String), :as_remote(context[:tx()]), :as_boolean(), :put(value::bool)]
   end
mkschulze commented 3 years ago

got it I think, can be closed.