BDD - Behat in PHP
https://www.youtube.com/watch?v=nBxnGXGEcwc&ab_channel=EscolaMobile
Behat on Laravel playground.
Stage 1 - BDD Setup
Install laravel like usually, for example using:
composer create-project --prefer-dist laravel/laravel behat-presentationBehat, as you will see soon is great test framework, but unfortunately poor supported by Laravel. There is only one library, that supports Behat on Laravel from laracast, but... It dosn't work with new Laravel version, and seems to be not supported anymore.
We can fix that by updating some code, so I've created repository fork for that. First, let's add repository to our composer file:
"repositories": [
{
"type": "vcs",
"url": "https://github.com/MWL91/Behat-Laravel-Extension"
}
]Now you are able to add required dev dependencies using:
composer require behat/behat behat/mink friends-of-behat/mink-extension laracasts/behat-laravel-extension:"dev-master as 1.1" --devAfter installation, we also need to configure our behat with behat.yml file. Create behat.yml file with contents below:
default:
extensions:
Laracasts\Behat:
env_path: .env.testing
Behat\MinkExtension:
default_session: laravel
laravel: ~Our testing env will be .env.testing. We also will use laravel session, in case of using Mink Extention that allows us to do browser tests.
Also in our test we want to use sqlite database, so we will change database config to:
And create a new database.sqlite in database directory.
Now we are ready to go with our environment using behat.
Stage 2 - Writing first scenario
Now, when we have our environment ready to go, we can start with initialize Behat.
This will create out first FeatureContext class, that we will use, to test our code.
We will start with extend FeatureContext class with Mink extension.
Now, when we run vendor/bin/behat, Bahat will tell us, that we don't have any scenarios and steps - time to create one.
Stage 2.1 - Writing first feature
In directory /features create file called rentacar.feature with contents below:
This describes what we are going to do. We will write simple solution for car rentals.
Now set some rules:
As you can see - it doesn't look like programming yet. It's more like a talk with customer or product owner. This makes Behat and BDD such a great tool! You have to first exactly know what you are going to do, and then you are allowed to start!
However, rules are only information for background, and are not interpreted by tests, but scenarios are.
Stage 2.2 - Writing first story
Let's try to create our first scenario to check first rule:
Now we have some business logic in our feature. Behat will recognize strings between "" and numbers. We may use also text string between """ ... """ tables, and <placeholders>.
So our scenario will be transformed with those parameters:
"Tabaluga Dragon"
1997
10
04
Now we have our first scenario. It was simple, isn't it? But I supposed that our testers, like Ania, will be not happy with that.
We also should test some failure scenarios.
As you can see, both scenarios are quite similar. We have another actor, another birth date, and negative as result. Behat, when we run tests, will do exactly the same methods that we used in success scenario.
Now, it's important, that you may not mix scenarios together. You have to think what exactly should be done in specific process.
For example
This scenario is valid, and may be processed but process is messy. In real check we don't want to have 2 actors. You may often have wrong scenarios for the first time, but BDD force you to do things right.
Stage 2.3 - gherkin language
Language that we are using is called "Gherkin". You can see the syntax using vendor/bin/behat --story-syntax.
If it's an issue, we don't have to even use English for that. Behat allows us to use for example polish syntax, you can view it using vendor/bin/behat --story-syntax --lang=pl.
What's funny, you can event write in Pirate English vendor/bin/behat --story-syntax --lang=en-pirate
Stage 3 - finally write some code
As you can see, we spend some time for writing scenarios, but we didn't write any code yet. Yes, and it's good approach! First, you need to define what you want to achieve, then you should write real code. This is like TDD but also you don't need to write test for code, that is not yet created. You don't need to use non-existing classes, to define system expectations - you can use human language. BDD makes you better developer. You have to first understood customer needs, then you are allowed to build solution.
Let's write some code. Using our feature file, we are able to generate code to test. Type vendor/bin/behat, then select FeatureContext, and you will see generated test methods:
We need them to describe steps in our process, otherwise Behat will tell us, that
FeatureContext has missing steps
You don't have to copy and paste them, you can automatically add them to FeatureContext, using
Stage 3.1 - first given handle
As you can see, now when you run vendor/bin/behat, Behat will tell you, that you need to write pending definition.
All your methods throws now PendingException - it's time to change that, let's start with first method.
As you can see, our sentence
there is a "Tabaluga Dragon", that was born in 1997-10-04
was changed to method thereIsAThatWasBornIn.
Behat automatically find that we have string parameter between "..." and found all numbers. In practice, we will have call like:
Tests, whether or not created under BDD, should work under Arrange - Act - Assert. Behat works the same way, and in next stage we implement that in code.
For current scenarios we used only one sentence for AAA, but often, you may need to use more. For that you may use And and But operators. There is no difference between them in practice. Those operators may be used also for Acting, or Asserting.
Stage 4 - Arrange – Act – Assert this is how the tests works
It's time to do implementation of our scenarios. Let's create Arrange step. Fill thereIsAThatWasBornIn with user factory. We added to user model birthday field, and cast it as date.
As we going to use sqlite to our tests, we may use DatabaseTransaction trait. Also, we should define user as class property.
Now when we run vendor/bin/behat you can see that we have first step pass.
Stage 4.1 - Act
Let's write implementation code for our test. We will do that by creating endpoint for car rent.
php artisan make:controller RentACarController --api
Then, add the endpoint to api routes:
Now let's add FormRequest for validating our credentials.
php artisan make:request RentACarRequest
And finally update store method in our controller:
So we have simple API that dosn't do anything smart yet, but respose with success or not. We can use it already test it with our scenario.
For that, we are going to use Laravel testing soultions, that are not prepared in the box like normal tests, so we will need to add some traits that laravel already has, and declare application.
Now when we run vendor/bin/behat we will have 2 steps passed. Now we need to write the last one, asserting.
Stage 4.2 - Assert
Now, final step - we need to check if our code works like expected.
After running behat we will receive:
So our both tests passed! As you see, we used the same methods for arrange, and act, but different for asserts. Now if you would like to implement more features, you can use the same syntax, and check other feature, without writing new pending definitions.
Stage 5 - second scenario
Let's write a new scenario for our second rule: Customer may rent one car at a time.
As you see, we need to write only one new definition to make it works, all others are already covered.
Now when we run vendor/bin/behat we see that our new scenario dosn't have step. When we select FeatureContext class, we will generate new definition, but as you see it'ss just one method.
Now let's update this, by adding user car property. We can use here database relations, but as it's just an example code, let's add just the name.
Now again let's run vendor/bin/behat. Our test of course didn't pass, because we didn't make required changes in code.
Let's fix that by changing RentACarRequest class.
We make some small refactor in our FormRequest. In real life, you probably don't want to check this in authorize method, but for our example this will be good enough.
Let's run behat vendor/bin/behat. All that works! Quietly fast like for a new feature in system, isn't it?
Stage 6 - last scenario
We already have validation tests, and we are not allowing users to rent a car if they are not matured, or already has one. Now it's time to write last scenario:
There are limited numbers of cars, customer may not rent reserved car.
For that we will need to create real application logic. But first, like in TDD, we need to write scenario:
First time we've used tables. After run vendor/bin/behat we'll generate new testing methods:
As you can see now in public function thereAreFollowingCars(TableNode $table) first parameter has now table attribute. We may use TableNode class like array. If we use it in foreach we get assoc table with keys car and qty. Also you can get an array using $table->getHash(), or if your table has indexes in rows $table->getRowsHash().
With that, we can create model and migration for cars: php artisan make:model Car -m.
Now, we are not going to create seeder for that, we just use input data from table, as thereAreFollowingCars.
Now when we have Arrange done, it's time to make Act step. But wait - we don't need to write any tests for Act - that was already done. This is cool, you don't need to repeat your tests steps.
Stage 6.1 - Asserting
Whatever you do TDD always force you to write code, that works. Even if now we have checks that works, but dosn't do anything smart, next scenario force us to do our job right. We can't anymore pretend that our code works, so let's update user migration, and add him car attribute.
php artisan migrate:fresh
Now in FeatureContext, let's add
Test will tell us, that we didn't assign car to user.
Let's to that by creating RentACarService that will handle our code.
And by updating controller:
Also now, we should add test to thereWillBeCarsAvailable.
After run vendor/bin/behat, our last scenario now working but... we have an issue with first test. We can fix it fast, by adding another Arrange sentence, so let's fix it:
Now we need to add new method to our FeatureContext
And that's it! All test now works.
And This is Behat! I hope you liked it :)
Last updated
Was this helpful?