acuminous / yadda

A BDD javascript library
412 stars 73 forks source link

Use Yadda to generate blank step files #89

Open simoami opened 10 years ago

simoami commented 10 years ago

I would like to know if I can take advantage of the parser to fetch the gwt steps so that I can generate empty step definitions.

I can retrieve the list of steps in a text form as shown below, but I still need to strip the GWT keywords from the step text in a way that supports localization.

screen shot 2014-04-27 at 1 09 25 pm

The idea goal would be to come up with an array of steps like this:

var steps = [
  { type: 'given', text: 'there are 100 bottles on the wall', raw: 'Given there are 100 bottles on the wall' },
  { type: 'when', text: '1 bottle falls off the wall', raw: 'When 1 bottle falls off the wall' },
  ...
];

If this is possible, can you give some pointers?

cressie176 commented 10 years ago

I can think of two ways you could use the parser to do this. The easiest is to generate the step definitions using library.define, instead of library.given, library.when, library.then. e.g.

library.define('Given user logs in with...');
library.define('And user goes to the "Sales"');

The results wont be as nice, but you won't have to worry about step matching or localisation at all. If that's not acceptable, then take a look at how I do this for the Pending annotation in Yadda/lib/plugins/mocha/BasePlugin...

    function is_pending(subject) {
        var regexp = new RegExp('^' + language.localise('pending') + '$');        
        for (var key in subject.annotations) {
            if (regexp.test(key)) return true;
        }
    }

You could iterate over the steps, and test to see whether the steps are given, when or then. However neither of these options will tell you whether a step is actually defined, so you'd be creating a lot of unnecessary noise.

What I might be able to do is raise an event when Yadda encounters a missing step. The event would contain the step text, and you could use either of the above tactics to generate the output in the event callback.

One problem with this approach is that depending on how you run Yadda it will fail fast, so you will only get the first missing step per scenario.

I had a look at doing something like this before, but can't remember why I parked it. It might have been because I didn't have an event system in place and didn't want to pollute the core code base with assumptions around Given / When & Then.

cressie176 commented 10 years ago

I've had another look. A better way than using the FeatureParser might be to change Interpreter.validate to accept a reporter argument. interpreter.validate gets called before Yadda runs a scenario, and currently generates a report like

[ { step: 'This is defined', valid: true },
  { step: 'This is undefined', valid: false, reason: 'Undefined Step' },
  { step: 'This is ambiguous', valid: false, reason: 'Ambiguous Step (Patterns [/[Tt]his is ambiguous/, /[tT]his is ambiguous/] are all equally good candidates)' } ]

If I can find a more sophisticated way to expose the report, you could parse it to generate the steps.

simoami commented 10 years ago

I took a stab at this and came up with an implementation that works great:

StepDefinitionGenerator.js

/*jshint node:true*/
/**
 * Generate step definitions from a feature object
 *
 * Usage:
 *     var generator = new StepDefinitionGenerator();
 *     generator.generate(feature, '{keyword}(\/{step}\/, function(done) {\n    done();\n});\n');
 *
 */
var Yadda = require('yadda');
var StepDefinitionGenerator = function(language) {
    this.language = language || 'English';
};
StepDefinitionGenerator.prototype.matchStep = function(step) {
    var prefix = '^(',
          suffix = ')\\s+(.+)$',
          keyword,
          _steps = ['given', 'when', 'then'];

    for(var i = 0, len = _steps.length; i < len; i++) {
         keyword = _steps[i];
         var re = new RegExp(prefix + Yadda.localisation[this.language].localise(keyword) + suffix);
         var match = re.exec(step);
         if(match) {
             return { type: keyword, keyword: match[1], step: match[2], raw: step};
         }
    }
    return null;
};
StepDefinitionGenerator.prototype.matchSteps = function(steps) {
    var me = this,
        matches = [];
    (steps||[]).forEach(function(step) {
        var match = me.matchStep(step);
        if(match) {
            matches.push(match);
        }
    });
    return matches;
};
StepDefinitionGenerator.prototype.getFeatureSteps = function(feature) {
    var me = this,
        steps = [];
    feature.scenarios.forEach(function(scenario) {
        steps = steps.concat(me.matchSteps(scenario.steps));
    });
    return steps;
};
StepDefinitionGenerator.prototype.generate = function(feature, stepTemplate) {
    var me = this,
        steps = me.getFeatureSteps(feature),
        output = '';
    steps.forEach(function(step) {
        output += stepTemplate.replace('{type}', step.type).replace('{keyword}', step.keyword).replace('{step}', step.step).replace('{raw}', step.raw) + '\n';
    });
    return output;
};

module.exports = StepDefinitionGenerator;

Usage:

var generator = new StepDefinitionGenerator();
generator.generate(feature, '// {raw}\nlibrary.{type}(\/{step}\/);\n');

Output:

// Given user logs in with credentials "username" and "password"
library.given(/user logs in with credentials "username" and "password"/);

// And user goes to the "Sales" module
library.given(/user goes to the "Sales" module/);

// When user attempts to create an invalid Sales order
library.when(/user attempts to create an invalid Sales order/);

// Then the Sales order creation fails with a validation message
library.then(/the Sales order creation fails with a validation message/);

The one possible improvement is to have access to the vocabulary._steps variable so I don't have to explicitly assume an array: ['given', 'when', 'then']

simoami commented 10 years ago

To your point, it would be good if the steps aren't just plain string entries, and your object structure makes sense. Another variation would be:

[ { step: 'This is defined', implemented: true, valid: true },
  { step: 'This is undefined', implemented: false, valid: false, reason: 'Undefined Step' },
  { step: 'This is ambiguous', implemented: true, valid: false, reason: 'Ambiguous Step (Patterns [/[Tt]his is ambiguous/, /[tT]his is ambiguous/] are all equally good candidates)' } ]
cressie176 commented 10 years ago

Nice work. I like that you've made the steps template a parameter. I also prefer making the implemented state explicit.

Yadda supports async and sync modes. One option would be not to generate the step callback, e.g.

// Given user logs in with credentials "username" and "password"
library.given(/user logs in with credentials "username" and "password"/);

It will have exactly the same effect as your output, but be compatible with both sync and async. How do you feel about not generating the callback?

simoami commented 10 years ago

Thanks!

yep, makes sense. The example was specific to a local example I was working on. Omitting the callback is a good idea as you suggest. I've updated the example above.

nonlux commented 7 years ago

I hardcoding same feature.

  1. I refused for default step definition.
    
    whenIGoToVVPage(page, context) {
    return BrowserSteps.load(Pages.get(page), context);
    },
vs 

library.when("I go to $page page", function(page) { return BrowserSteps.load(Pages.get(page), this.context); });


code in this [gist](https://gist.github.com/nonlux/6f0787190407e7356aab3d94313de1bb#file-library-js)

2.  I add  step generator [gist](https://gist.github.com/nonlux/6f0787190407e7356aab3d94313de1bb#file-yadda_runner-js)

After that, the test recording speed increased several times.

For next feature I am planning fix  generation with AST parsing.