Cucumber-rust since 0.7 – The Most Important Changes

cucumber-rust has had a long way, since my last post about the 0.7 release in October 2020. It’s time to come back and see what happened since back then. First of all, starting from the initial 0.8.0 release, I will dig through the changelog and evaluate my favorite changes. Then we will update the Cucumber tests of my encrspyter project to the most recent version. Lots of stuff to do, so let’s go!

New things first

Let’s start soft: With 0.8.4, we got a --debug command line flag that leverages the test execution to nicely print stdout and stderr for each executed step. We can activate the debug mode in the runner creation code of our test’s main function:

fn main() {
    let runner = cucumber::Cucumber::<EncrsypterTestWorld>::new()
        .features(&["./tests/features/"])
        .steps(encrypt_decrypt_steps::steps())
        .debug(true); // This activates the new debug mode 
    ...
}

By running cargo test, we can see it in action:

Cucumber-rust's Debug mode produces sections in the test's cli output called Captured stdout and Captured stderr respectively. Captured stdout contains stdout text in white, Captured stderr contains stderr text in blue.

Neat, right? 🙂

t!-Macro extended with a World parameter type

Tiny but neat addition: We can now add the type of our Cukes World-object to the t!-closure.

t!(|mut world: MyPersonalCukesWorld, ctx| { [...] }

Although the generated code is the same as without the explicit type, it adds a bit more Rust-style expressivity. Sweet!

New callback methods for the Cucumber runner: before and after

In vanilla Cucumber, I admired its feature to define hooks that intercept the execution of a feature or a scenario. You can write some code and tell Cucumber to execute it before, after or before and after a scenario, feature or even a step. This is useful to for example set up or tear down a test database before or respectively after a test run.

With the release of 0.9.0, we can do similar things in Rust, too. There is a significant implementation difference to vanilla Cukes though: Our hooks won’t be picked up from wherever they are defined, but are defined as properties of the Cucumber runner instead. To compensate, our before and after hooks come with powerful query options to decide where to execute the defined method.

The second difference is that they are not officially called “hooks” but “lifecycle methods” instead. I might get this wrong due to habits. Please bear with me. 😄

Lets head into an example. Given 2 features, one of them in English, one of them in German, each in 2 separate files:

# Feature 1 (English description)
Feature: Encrypt messages and write them to a file.

  Scenario: Encrypt a simple Hello World - message.
    Given I have an encryptor initialized with input "Hello World!"
     When I test print to STDOUT
      And I test print to STDERR
     Then I should see "Hello World!" in the test encryptor's input field
     When I encrypt the encryptor's input
     Then testfile.txt exists
      And testfile.txt is not empty
     When I decrypt testfile.txt
     Then the decrypted result should be "Hello World!"
# language: de
# Feature 1 (German description)
Funktionalität: Verschlüssele Nachrichten und schreibe sie in eine Datei.

  Beispiel: Encrypt a simple Hello World - message.
    Angenommen I have an encryptor initialized with input "Hello World!"
     Wenn I test print to STDOUT
      Und I test print to STDERR
     Dann I should see "Hello World!" in the test encryptor's input field
     Wenn I encrypt the encryptor's input
     Dann testfile.txt exists
      Und testfile.txt is not empty
     Wenn I decrypt testfile.txt
     Dann the decrypted result should be "Hello World!"

What we want to do now is get greeted and dismissed in the respective language. We will define proper lifecycle methods on our Cucumber runner to do that. In the main method:

    let english_feature_name = "Encrypt messages and write them to a file."; // full string filter for the English...
    let german_feature_pattern = Regex::new("Verschlüssele Nachrichten.*").unwrap(); // and a Regex filter for the German variant.

let runner = cucumber::Cucumber::<world::EncrsypterTestWorld>::new()
.features(&["./tests/features/"])
        .steps(crate::encrypt_decrypt_steps::steps())
        .language("de") 
        .before(feature(english_feature_name), |_ctx| {
            async { println!("Greetings, encryptor!") }.boxed()
        })
        .after(feature(english_feature_name), |_ctx| {
            async { println!("Goodbye, encryptor!") }.boxed()
        })
        .before(feature(german_feature_pattern.clone()), |_ctx| { // clone is necessary here due to the trait bounds of Inner<Pattern>
            async { println!("Hallo, Verschlüsselnder.") }.boxed()
        })
        .after(feature(german_feature_pattern), |_ctx| {
            async { println!("Tschüss, Verschlüsselnder.") }.boxed()
        });

feature() expects either the full feature description as a &str or a valid regex::Regex() matching your targets’ description string. The latter requires the regex module as a dependency in your Cargo.toml, but it will provide you a highly powerful filtering tool, so adding that additional dependency is highly recommended.

Executing cargo test will show us what we expect. For the English feature file:

Greetings, encryptor!
Feature: Encrypt messages and write them to a file.

[...]

  ✔ Then the decrypted result should be "Hello World!"                                                                
Goodbye, encryptor!

For the German Feature file:

Hallo, Verschlüsselnder.
Funktionalität: Verschlüssele Nachrichten und schreibe sie in eine Datei.

[...]

  ✔ Dann the decrypted result should be "Hello World!"                                                               
Tschüss, Verschlüsselnder.

Great stuff! Last but not least, let me note that this does not only work with Feature, but with Scenario and Rule, too. You can even create more custom filters by combining them with And and Or. Please refer to the cucumber-rust code base for more about that.

Heads up, a breaking change! 👷🏻‍♂️

With 0.9.0 we got one significant change in Cukes’ public API, but don’t worry: Fixing it is quickly done and even quite easily automatable. If you review my guide on cucumber-rust for 0.7, you will see the related step definitions written like this:

.given_regex_async(
    r#"^I have an encryptor initialized with input "([\w\s!]+)"$"#,
    t!(|mut world, texts_to_encrypt, _step| {
        world.encryptor.input = Cow::Owned(texts_to_encrypt[1].to_owned());
        world
    }),
)

This throws a compiler error now stating that the “signature” of the t! macro has changed: Instead of the regex matches object in parameter #2 and _step in parameter #3, we now have a single StepContext object that contains the properties matches and step.

Therefore, in the above example we have to do the following:

  1. Remove the _step parameter entirely
  2. Rename our matches parameter texts_to_encrypt to something that reflects the StepContext type: ctx
  3. Replace the occurrences of texts_to_encrypt with ctx.matches[index_used_previously]

For _step we have no replacements to do, because we didn’t use it in the first place, so that’s basically it. The runnable step definition should now look like this:

.given_regex_async(r#"^I have an encryptor initialized with input "([\w\s!]+)"$"#, t!(|mut world, ctx| {
                world.encryptor.input = Cow::Owned(ctx.matches[1].to_owned());
                world
    }),
)

Personally I like this particular change quite a lot, because it keeps the already loaded t! macro clean and organised. What do you think? Feel free to let me know in the comments below.

Feature: Add before and after lifecycle functions to the Cucumber builder. This function takes a selector for determining when to run 'before' or 'after', and a callback

Feature: add language argument to Cucumber builder to set default language for all feature files (ON HOLD)

Encrsypter’s Cucumber tests in a new look

I updated the tests in Encrsypter’s project master and in the cukes_0.9.0 branch, so if you want to see the changes in full action, give it a git pull on the master or a git checkout on the mentioned branch and enjoy. 🙂

Conclusion: great changes and improvements

Phew, so long. cucumber-rust really does have a long way, and many things have changed for more Cukes excitement. Personally I like the current implementation state really a lot and I’m looking forward to seeing its bright future. But for now, let’s wrap up the wrapup, shall we?

If you want to read more about Cukes in Rust, here’s my intro to Cucumber in Rust written for 0.7. Or you might say “meh, I prefer the vintage things of life, give me the vanilla stuff”. In that case, you can find the original version of my intro guide here.
And last but for sure not least, here’s the project’s full changelog with all the goodness listed. Happy cuking!

Share it with:

Playwright for Browser Automation

Last week I held a short & sweet presentation in the company about the usage, benefits and drawbacks of Browser Remote Debugging APIs. One unfortunate problem we discovered was the lack of a standard across the browsers; every major browser maintains its very own implementation. The RemoteDebug – Intitiative tried to solve this problem, but until now without noticeable success, as you can see here by the lack of activity. Therefore, the Test and Development – World needed to deal with that all by themselves. A great team of ex Puppeteer-developers, who moved from Google to Microsoft, did exactly that by bringing us Playwright, a framework for writing automated tests encapsulating and using the various Remote Debugging Interfaces. In today’s short example we write a quick example test with Playwright.

Installing Playwright

As a starting prerequisite, we need a NodeJS-Distribution with Version 10 or greater. Next, we go to our already well-filled project directory and create a new NodeJS-Project:

$ cd /path/to/your/project/directory
$ mkdir playwright_test && cd playwright_test
$ npm init
$ npm install --save-dev playwright

While the installation progresses, you will notice that Playwright brings its own browser binaries. Don’t worry about that, they are still perfectly valid, as the rendering engines are not modified at all. Only the debugging capabilities have been given a few extensions.

Alright, that’s all we need.

Time to dive into the code!

Let’s assume we want to buy red shoes on Amazon, because we need new shoes, and red is a nice color.

// 1. We start by initializing and launching a non-headless Firefox 
// for demo purposes.
// (How do you call them, "headful"? "headded"? Feel free to drop me 
// your best shots. :))
const {firefox} = require("playwright");

(async () => {
  const browser = await firefox.launch({headless: false, slowMo: 50});
  const context = await browser.newContext();

  // 2. Next, we head to the Amazon Landing Page...
  const page = await context.newPage();
  await page.goto("https://www.amazon.com");
  
  // 3. ...do the search for Red Shoes...
  await page.fill("#twotabsearchtextbox", "Red Shoes");
  const searchBox = await page.$("#twotabsearchtextbox");
  await searchBox.press("Enter");

  // 4. ...and take a nice deep look at the page 
  // by saving a screenshot.
  await page.waitFor("img[data-image-latency='s-product-image']");
  await page.screenshot({path: "./screenshot.jpg", type: "jpeg"});
  
  // 5. Afterwards, we leave the testrun with a clean state.
  await browser.close();
})();

That’s it for now. From here, we can extend the test by doing elaborate verification steps, check out a nice pair of red shoes and pay them with our hard-earned testing money.  Feel free to check out the example’s full source code from here and go ham.

Conclusion

With Playwright we got a means to write automated tests with ease against the many different Remote Debugging APIs. It copes nicely with the API differences while preserving an intuitive and familiar JS test automation syntax.

So if you are looking for a more lightweight and lower level alternative to Selenium, give it a go!

Share it with:

Zalenium in a minimal Docker Compose – Setup

What is Zalenium?

Zalenium, brought to us by German online fashion retailer Zalando, is a feature-enriched Selenium test platform based on the popular Selenium Grid. Besides the core features like scaling Selenium test execution nodes, it provides nice things like video recording, a video player directly in the management UI and integrations with popular browser test tools like Sauce Labs. For a more detailed overview, please check out the project page. As far as we are concerned here, we have all the good arguments we need to fire up a small test setup. 🙂

What are we going to do?

In the following miniworkshop, we temporarily slip into the shoes of a devops engineer and set up a minimal Zalenium Grid – environment in order to execute remote Selenium tests there. The goal is that we use no more than 2 files (of resonable size):

  • the docker-compose-file to build and start the Zalenium-container provided by Zalenium
  • a sample selenium-webdriver-test to be executed inside Zalenium, kindly provided by Felipe Almeida, thank you very much.

For our experiment, I modified the latter to enable remote driver execution instead of starting a local firefox. Therefore, I prepared everything in a small bitbucket-repo.

Prereqs for the Zalenium Setup

  • a recent version of Docker (should already include docker-compose)
  • Ruby > 2.3.1 (I recommend using RVM)
  • a recent Chrome-browser
    • Unfortunately, my Firefox (v67.0.4) does not support the video format of the test execution recordings. 🙁

Steps

  1. Open a terminal and clone the repo.
  2. cd inside the new directory and fire up the containers: $ docker-compose up -d
  3. Start the test: $ ruby selenium_minimal.rb
  4. After the test execution, open a Chromeand head to the Dashboard: http://localhost:4444/dashboard/
  5. You should see one test execution in the list on the left side. Click it.
  6. Play the video and enjoy the action of your test.

Conclusion

Now that you have the power to quickly fire up Zalenium and its grid nodes, you can go further. Host it on a remote machine serving your needs as a Test Automaton Engine, move it to the cloud and go to town. This should step up your Quality Assurance Game in a scalable and easily maintainable way. Have fun!

Share it with: