Package: behaving
behavingbehavingis a web application testing framework for
Behavior-Driven-Development, based onbehaveandsplinter.behaveis written in Python and is similar toCucumber.behavingadds the step-libraries for multi-user web/email/sms/gcm
interactions, and provides the Pythonbehavingnamespace so that
independent step-libraries can work together.Please refer tobehave's excellentdocumentationfor a guide on
how to use it, how to write your custom steps and make it possible to
extendbehaving.Hello worldStarting to usebehavingis pretty easy. Inside some python module,
add yourfeaturesconsisting each of one or more scenarios. These
features are Gherkin language files with an extension of.feature. In
the same directory you should have a steps module which imports thebehavingsteps as well as your own custom steps (more on that later in
the setup_ section) . Here's a basic example:Feature:Text presenceBackground:Givena browserScenario:Search for BDDWhenI visit "http://www.wikipedia.org/"AndI fill in "search" with "BDD"AndI press "go"ThenI should see "Behavior-driven development" within5secondsEmail, SMS & GCM (Google Cloud Messaging)While the web is the focus ofbehaving, it also includes simple mocks
for a mail, SMS and a GCM server. These come with a small collection of
steps allowing you to do things like:Feature:Email & SMSScenario:Click link in an emailGivena browserWhenI send an email to "foo@bar.com" with subject "Hello" and body "Try out this website at http://google.com"AndI click the link in the email I received at "foo@bar.com"Thenthe browser's URL should be "http://google.com/"Scenario:Receive SMS with bodyWhenI send an sms to "+4745690001" with body "Hello world"ThenI should receive an sms at "+4745690001" containing "world"Scenario:Receive GCM NotificationWhenI send a gcm message "{"to":"deviceID","data": {"message":"Foo Bar","badge": 6}}"ThenI should receive a gcm notification at "deviceID" containing "{'data': {'message': 'Foo Bar'}}"Typically, it will be your web application that sends
email/sms/notifications and testing it comes down to configuring the
application to send email/sms/notifications to the mock servers.Personas & stateA lot of web apps today rely on multi-user interactions. To help you
with those interactions,behavinguses the notion ofpersonas. A
persona within a test runs in its own instance of a browser and you can
have more than one persona (and its browser instance) running
concurrently. You switch among personas by callingGiven"PersonaName" as the personaPersonas are also typically implemented as simple dictionaries allowing
them to carry state, save and reuse variables inside a scenario. When a
persona is first invoked it is created as an empty dictionary. You can
predefine personas though with set values.Let's take the familiar LOTR characters as our test users. On setting up
the test environment (details later in the setup_ section), we set up
the characters basic variables we might be needing in the tests as such:PERSONAS={'Frodo':dict(fullname=u'Frodo Baggins',email=u'frodo@shire.com',password=u'frodopass',mobile='+4745690001',address:{street:"The Shire",zip:"4321"}),'Gandalf':dict(fullname=u'Gandalf the Grey',email=u'gandalf@wizardry.com',password=u'gandalfpass',mobile='+4745690004',address:{street:"Rivendell street 1",zip:"1234"}),...}defbefore_scenario(context,scenario):...context.personas=PERSONASWithin a test and given a persona, you can now use$var_nameto access
a variable of a persona. You can also set new variables on personas. So
the following,Given"Gandalf" as the personaWhenI fill in "name" with "$fullname"AndI fill in "street" with "$address.street"AndI set "title" to the text of "document-title"AndI fill in "delete" with "$title"AndI set "address.country" to the text of "country"AndI set "postaddress" to:"""$fullname$address.street, $address.zip, $address.country"""would fill in the field with idnamewithGandalf the Grey,streetwithRivendell street 1set the variabletitleto the text of the
element with iddocument-titleand reuse the variabletitleto fill
in the field with iddelete. It would also store the value of the
field with id "country" in address[country]. The$var_namepattern
is also usable in the text received by steps that expect a body of text,
which means that thepostaddresspersona variable will contain
Gandalf's complete snail-mail postage address nicely formatted on
multiple lines.Hello Persona exampleLet us assume the following (coming from a real example) scenario.Crypho, is an online messaging/sharing site that
provides users with end-to-end encrypted real-time communications.behavingwas written to help test Crypho.In Crypho, teams collaborate inspaces. To invite somebody in aspacethe invitee has to share a token with an invitor, so both can
verify each other's identity.Feature:Frodo invites Gandalf to The Shire spaceGiven state "the-shire"Scenario:Frodo invites Gandalf to The ShireGiven"Gandalf" as the personaWhenI log inBefore the scenarios start, the custom stepGiven state "the-shire"executes. This preloads the db with data, sets up the server etc. Then
the scenario executes:First Gandalf logs in. The stepGiven "Gandalf" as the persona, fires
up a browser that belongs to the persona Gandalf. The following step,When I log inis a custom step defined as follows:@when('I log in')deflog_in(context):assertcontext.personacontext.execute_steps(u"""When I go to HomeThen I should see an element with id "email" within 2 secondsWhen I fill in "email" with "$email"And I press "send-sms"Then I should see "We have sent you an SMS with a security code" within 2 secondsAnd I should receive an sms at "$mobile"And "token" should be enabledWhen I parse the sms I received at "$mobile" and set "Your Crypho code is{token}"And I fill in "token" with "$token"And I fill in "password" with "$password"And I press "login"Then I should see "Crypho" within 5 seconds""")Observe above how the current persona (Gandalf) parses the sms it
receives and saves it as "token". Later Gandalf reuses it to fill in the
two-factor authentication field.Now that Gandalf is logged in, the test proceeds with Frodo. Frodo will
log in, and invite Gandalf to a private space.Given"Frodo" as the personaWhenI log inAndI click the link with text that contains "My spaces"AndI click the link with text that contains "The Shire"AndI press "invite-members"ThenI should see "Invite members" within1secondsWhenI fill in "invitees" with "gandalf@wizardry.com"AndI fill in "invitation-message" with "Come and join us!"AndI press "send-invitations"ThenI should see "Your invitations have been sent" within2secondsOnce the invitations are sent we switch back to Gandalf's browser, who
should have received a notification in his browser, as well as an email.
He then proceeds to send an sms to Frodo with the token who completes
the invitation.Given"Gandalf" as the personaThenI should see "Your invitations have been updated" within2secondsAndI should receive an email at "gandalf@wizardry.com" containing "Frodo Baggins has invited you to join a private workspace in Crypho"WhenI click the link with text that contains "Invitations"AndI click the link with text that contains "Pending invitations"ThenI should see "Come and join us!"WhenI set "token" to the text of "invitation-token"AndI send an sms to "45699900" with body "$token"Given"Frodo" as the personaThenI should receive an sms at "45699900"WhenI set "FrodoToken" to the body of the sms I received at "45699900"AndI click the link with text that contains "Invitations"AndI click the link with text that contains "Enter authorization token"AndI fill in "auth-token" with "$FrodoToken"AndI press "Submit"ThenI should see "The invitation has been accepted." within5secondsAndI should see "Gandalf the Grey has joined the space, invited by Frodo Baggins" within10secondsYou can see the test in action on videohere.Setting up a test environmentStart by installingbehavingby using eitherpiporeasy_install.
This will also install dependencies and create thebehavescript with
which you invoke your tests. If you prefer using buildout, clone the
package itself from its repository, it contains already a buildout
configuration.Typically you will be having a folder containing all your features and
steps. For example a directory structure like the following:features/
features/mytest.feature
features/myothertest.feature
features/environment.py
features/steps/
features/steps/steps.pyIn the steps directory you will need to import thebehavingsteps you
need. You can also define your own steps. Sosteps.pymight look like:frombehaveimportwhenfrombehaving.web.stepsimport*frombehaving.sms.stepsimport*frombehaving.mail.stepsimport*frombehaving.notifications.gcm.stepsimport*frombehaving.personas.stepsimport*@when('I go to home')defgo_to_home(context):context.browser.visit('https://web/')Inenvironment.pyyou specify settings as well the things that need to
happen at various stages of testing, i.e. before and after everything, a
feature run, or a scenario run. For convenience you can import and reusebehaving.environmentwhich will perform default actions like closing
all browsers after a scenario, clean the email folder etc.It is also possible to usebehaving.web.environment,behaving.mail.environment,behaving.sms.environmentandbehaving.personas.environmenton their own, if you don't have need for
SMS for example.An example of an environment that does simply set some variables and
then rely on default actions for the various stages, might look like the
following:importosfrombehavingimportenvironmentasbenvPERSONAS={}defbefore_all(context):importmypackagecontext.attachment_dir=os.path.join(os.path.dirname(mypackage.__file__),'tests/data')context.sms_path=os.path.join(os.path.dirname(mypackage.__file__),'../../var/sms/')context.gcm_path=os.path.join(os.path.dirname(mypackage.__file__),'../../var/gcm/')context.mail_path=os.path.join(os.path.dirname(mypackage.__file__),'../../var/mail/')benv.before_all(context)defafter_all(context):benv.after_all(context)defbefore_feature(context,feature):benv.before_feature(context,feature)defafter_feature(context,feature):benv.after_feature(context,feature)defbefore_scenario(context,scenario):benv.before_scenario(context,scenario)context.personas=PERSONASdefafter_scenario(context,scenario):benv.after_scenario(context,scenario)The following variables are supported and can be set to override
defaults:screenshots_dir(the path where screenshots will be saved. If it
is set, any failure in a scenario will result in a screenshot of the
browser at the time when the failure happened.)attachment_dir(the path where file attachments can be found)sms_path(the path to be used bysmsmockto save sms. Defaults
tocurrent_dir/sms)gcm_path(the path to be used bygcmmockto save gcm
notifications. Defaults tocurrent_dir/gcm)mail_path(the path to be used bymailmockto save mail.
Defaults tocurrent_dir/mail)default_browserdefault_browser_size(tuple (width, height), applied to each
browser as it's created)max_browser_attempts(how many times to retry creating the browser
if it fails)remote_webdriver_url(points to your selenium hub url or remote
webdriver. Defaults toNone)browser_args(a dict of additional keyword arguments used when
creating a browser)base_url(the base url for a browser, allows you to use relative
paths)accept_ssl_certs(setting toTruewill accept self-signed/invalid
certificates. Defaults toNone)You can run the tests simply by issuing./bin/behave./featuresFor chrome and docker issues, the code below is usefulfromselenium.webdriver.chrome.optionsimportOptionschrome_options=Options()chrome_options.add_argument('--no-sandbox')context.browser_args={'options':chrome_options}Mail, GCM and SMS mock serversWhenbehavingis installed, it creates three scripts to help you test
mail, gcm and sms,mailmock,gcmmockandsmsmockrespectively. You
can directly invoke them before running your tests, they all take a port
as well as the directory to output data as parameters. For example,./bin/smsmock-p8081-o./var/sms
./bin/gcmmock-p8082-o./var/notifications/gcm
./bin/mailmock-p8083-o./var/mail[--no-stdout]behaving.webSupported matchers/stepsBrowsersGiven a browser [opens the default browser, i.e. Firefox]Givenbrandas the default browser [sets the default browser to bebrand, this is the browser name when using the remote webdriver or Firefox, Chrome, Safari]Given the electron app "app_path" [for use with electron-based desktop apps]Given browser "name" [opens the browser namedname]When I reloadWhen I go backWhen I go forwardWhen I resize the browser towidthxheightWhen I resize the viewport towidthxheightWhen I take a screenshot [will save a screenshot of the browser ifscreenshots_diris set on the environment. Also, ifscreenshots_diris set, all failing tests will result in a screenshot.]When I execute the script "script"When I set the cookie "key" to "value"When I delete the cookie "key"When I delete all cookiesWhen I close the browser "name"FramesWhen I switch to frame with css "css"When I switch back to the main pageWindowsWhen I open a new window named "name" at "url"When I name the current window "name"When I switch to the window named "name"URLsGiven the base url "url" [sets the base url tourl, alternatively setcontext.base_urldirectly inenvironment.py]When I visit "url"When I go to "url"When I parse the url path and set "{expression}"Then the browser's URL should be "url"Then the browser's URL should contain "text"Then the browser's URL should not contain "text"LinksWhen I click the link to "url"When I click the link to a url that contains "url"When I click the link with text "text"When I click the link with text that contains "text"Text, element & class presenceWhen I wait fortimeoutsecondsWhen I show the element with id "id"When I hide the element with id "id"TextThen I should see "text"Then I should not see "text"Then I should see "text" withintimeoutsecondsThen I should not see "text" withintimeoutsecondsIDThen I should see an element with id "id"Then I should not see an element with id "id"Then I should see an element with id "id" withintimeoutsecondsThen I should not see an element with id "id" withintimeoutsecondsCSSExistenceThen I should see an element with the css selector "selector"Then I should not see an element with the css selector "selector"Then I should see an element with the css selector "selector" withintimeoutsecondsThen I should not see an element with the css selector "selector" withintimeoutsecondsThen I should seenelements with the css selector "css"Then I should see at leastnelements with the css selector "css" withintimeoutsecondsVisibilityThen the element with the css selector "css" should be visibleThen the element with the css selector "css" should be visible withintimeoutsecondsThen the element with the css selector "css" should not be visibleThen the element with the css selector "css" should be visible withintimeoutsecondsThen {n:d} elements with the css selector "css" should be visibleThen {n:d} elements with the css selector "css" should be visible withintimeoutsecondsThen at least {n:d} elements with the css selector "css" should be visibleThen at least {n:d} elements with the css selector "css" should be visible withintimeoutsecondsExistence of a class on an elementThen the element with xpath "xpath" should have the class "cls"Then the element with xpath "xpath" should not have the class "cls"Then the element with xpath "xpath" should have the class "cls" withintimeoutsecondsThen the element with xpath "xpath" should not have the class "cls" withintimeoutsecondsThen "name" should have the class "cls"Then "name" should not have the class "cls"Then "name" should have the class "cls" withintimeoutsecondsThen "name" should not have the class "cls" withintimeout:dsecondsXPathThen I should see an element with xpath "xpath"Then I should not see an element with xpath "xpath"Then I should see an element with xpath "xpath" withintimeoutsecondsThen I should not see an element with xpath "xpath" withintimeoutsecondsFormsWhen I fill in "name|id" with "value"When I clear field "name|id"When I type "value" to "name|id" [same as fill, but happens slowly triggering keyboard events]When I choose "value" from "name"When I check "name|id"When I uncheck "name|id"When I toggle "name|id"When I select "value" from "name""When I select by text "text" from "name""When I press "name|id|text|innerText"When I press the element with xpath "xpath"When I attach the file "path" to "name"When I set the innner HTML of the element with id "id" to "contents" [Sets html on acontenteditableelement with ididtocontents]When I set the innner HTML of the element with class "class" to "contents"When I set the innner HTML of the element with class "class" to "contents"When I send "KEY" to "name"When I focus on "name"Then field "name" should have the value "value"Then field "name" should have the value "value" withintimeoutsecondsThen the selection "name" should have the options "valueA, valueB" selectedThen "name" should be enabledThen "name" should be disabledThen "name" should not be enabledThen "name" should be validThen "name" should be invalidThen "name" should not be validThen "name" should be requiredThen "name" should not be requiredHTML tablesThen the table with id "id" should be| header1 | header2 | ... | header(m) || cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |...| celln0 | celln1 | ... | cellnm |Then the table with xpath "xpath" should be| header1 | header2 | ... | header(m) || cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |...| celln0 | celln1 | ... | cellnm |Then the table with id "id" should contain the rows| cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |Then the table with xpath "xpath" should contain the rows| cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |Then the table with id "id" should not contain the rows| cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |Then the table with xpath "xpath" should not contain the rows| cell00 | cell01 | ... | cell0m || cell10 | cell11 | ... | cell1m |Then rowrow_noin the table with id "id" should be| cell00 | cell01 | ... | cell0m |Then rowrow_noin the table with xpath "xpath" should be| cell00 | cell01 | ... | cell0m |Then the value of the cell in rowrow_no, columncol_noin the table with id "id" should be "value"Then the value of the cell in rowrow_no, columncol_noin the table with xpath "xpath" should be "value"Then the value of the cell in rowrow_no, column "col_header" in the table with id "id" should be "value"Then the value of the cell in rowrow_no, column "col_header" in the table with xpath "xpath" should be "value"Alerts & promptsWhen I enter "text" to the alert - When I accept the alert - When I dismiss the alert - Then I should see an alert - Then I should see an alert withintimeoutseconds - Then I should see an alert containing "text" - Then I should see an alert containing "text" withintimeoutsecondsMouseWhen I mouse over the element with xpath "xpath"When I mouse out of the element with xpath "xpath"DownloadsThen the file "filename" with contents "text" should have been downloaded withintimeoutsecondsThen the file "filename" should have been downloaded withintimeoutsecondsPersona interaction & variablesWhen I set "key" to the text of "id|name"When I set "key" to the attribute "attr" of the element with xpath "xpath"When I evaluate the script "script" and assign the result to "key"behaving.mailSupported matchers/stepsWhen I click the link in the email I received at "address"When I parse the email I received at "address" and set "expression"When I clear the email messagesThen I should receive an email at "address"Then I should receive an email at "address" with subject "subject"Then I should receive an email at "address" containing "text"Then I should receive an email at "address" with attachment "filename"Then I should not have received any emails at "address"behaving.smsSupported matchers/stepsWhen I set "key" to the body of the sms I received at "number"When I parse the sms I received at "number" and set "expression"Then I should receive an sms at "number"Then I should receive an sms at "number" containing "text"behaving.notifications.gcmSupported matchers/stepsWhen I send a gcm message "{"to":"deviceID", "data": {"message":"Foo Bar", "badge": 6}}"Then I should receive a gcm notification at "deviceID" containing "{'data': {'message': 'Foo Bar'}}"Then I should have received any gcm notifications at "deviceID"behaving.personasSupported matchers/stepsGiven "name" as the personaWhen I set "key" to "value"When I set "key" to:"""some longer body of textusually multiline"""When I clone persona "source" to "target"Then "key" is set to "value"DebuggingWhen I pause the testsDocker integrationADockerfileas well as a complete setup usingdocker-composeare provided to help you create selenium grid configurations that run your tests. In addition dev container configuration is included if VSCode is your thing.In addition we provide pre-build images on docker hub for thelinux/amd64andlinux/arm64platforms. Usedockerpullbehaving/behaving:latestto pull the image.Running behaving testsYou can run all behaving tests as follows:Start docker compose:docker-compose upOpen a shell in the behaving container:docker-compose exec behaving bashRun behaving tests:behave tests/features