Python, Selenium Grid and Docker

With Docker you can quickly and easily install, configure and use Selenium Grid. This tutorial shows the respective steps that you need as a software tester (or Developer). Instead of Python you can also use other languages, which are supported by Selenium​.

Preconditions

Preparation of files

# create new project
$ mkdir -p ~/Project/SeleniumTutorial && cd ~/Project/SeleniumTutorial

# create docker-compose.yml (version 1)
$ vim v1-docker-compose.yml

# or create docker-compose.yml (version 2)
$ vim v2-docker-compose.yml

# create python example.py
$ vim example.py

Note: You can opt for a version of docker-compose.yml!

Version: 1

---
selenium_hub:
  image: selenium/hub
  ports:
    - 4444:4444
node_1:
  image: selenium/node-chrome
  links:
    - selenium_hub:hub
node_2:
  image: selenium/node-firefox
  links:
    - selenium_hub:hub

Version: 2

---
version: '2'
services:
  selenium_hub:
    image: selenium/hub
    ports:
      - 4444:4444
  node_1:
    image: selenium/node-chrome
    depends_on:
      - selenium_hub
    environment:
      - HUB_PORT_4444_TCP_ADDR=selenium_hub
  node_2:
    image: selenium/node-firefox
    environment:
      - HUB_PORT_4444_TCP_ADDR=selenium_hub
    depends_on:
      - selenium_hub
import os
import datetime
import time
import unittest
from selenium import webdriver


class Example(unittest.TestCase):

    def setUp(self):

        self.driver = webdriver.Remote(
            command_executor='http://192.168.99.100:4444/wd/hub',
            desired_capabilities={
                'browserName': 'firefox',
                'javascriptEnabled': True
            }
        )

        self.driver.get('http://softwaretester.info/')

    def test_something(self):

        dt_format = '%Y%m%d_%H%M%S'
        cdt = datetime.datetime.fromtimestamp(time.time()).strftime(dt_format)
        current_location = os.getcwd()
        img_folder = current_location + '/images/'

        if not os.path.exists(img_folder):
            os.mkdir(img_folder)

        picture = img_folder + cdt + '.png'
        self.driver.save_screenshot(picture)

    def tearDown(self):

        self.driver.quit()


if __name__ == "__main__":

    unittest.main(verbosity=1)

Create environment

# create new VM
$ docker-machine create -d virtualbox Grid

# pointing shell
$ eval $(docker-machine env Grid)

# show status (optional)
$ docker-machine ls
...
NAME   ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER    ERRORS
Grid   *        virtualbox   Running   tcp://192.168.99.100:2376           v1.11.1 

# run docker-compose (Version: 1)
$ docker-compose -f v1-docker-compose.yml up -d

# run docker-compose (Version: 2)
$ docker-compose -f v2-docker-compose.yml up -d

# show status (Version: 1)
$ docker-compose -f v1-docker-compose.yml ps
...
             Name                         Command           State           Ports          
------------------------------------------------------------------------------------------
seleniumtutorial_node_1_1         /opt/bin/entry_point.sh   Up                           
seleniumtutorial_node_2_1         /opt/bin/entry_point.sh   Up                           
seleniumtutorial_selenium_hub_1   /opt/bin/entry_point.sh   Up      0.0.0.0:4444->4444/tcp

# show status (Version: 2)
$ docker-compose -f v2-docker-compose.yml ps
...
             Name                         Command           State           Ports          
------------------------------------------------------------------------------------------
seleniumtutorial_node_1_1         /opt/bin/entry_point.sh   Up                           
seleniumtutorial_node_2_1         /opt/bin/entry_point.sh   Up                           
seleniumtutorial_selenium_hub_1   /opt/bin/entry_point.sh   Up      0.0.0.0:4444->4444/tcp

Open Browser

Selenium Grid Console

Run Python script

# run python selenium script
$ python -B ~/Projects/Selenium/example.py

Note: Via browserName (example.py) you can choose the respective browser (firefox or chrome)!

Note: Via docker-compose scale you can add/remove node instances!

# create 2 instances (Version: 1)
$ docker-compose -f v1-docker-compose.yml scale node_1=2

# create 3 instances (Version: 2)
$ docker-compose -f v2-docker-compose.yml scale node_2=3

Create REST API mock server with Docker

This time again a tutorial with various instructions. It is a REST API services for development and testing purposes and some simple Docker instructions.

Preconditions

Note: For Mac OS X and Windows use Docker Toolbox!

Create and connect into Boot2Docker VM

# create new boot2docker vm
$ docker-machine create -d virtualbox apivm

# list created vm(s)
$ docker-machine ls

# list informations (optional)
$ docker-machine inspect apimock

# ssh into boot2docker vm
$ docker-machine ssh apivm

Create Dockerfile (inside VM)

# create new Dockerfile
$ vi Dockerfile
FROM ubuntu

# install python packages
RUN apt-get update && apt-get -y install python python-dev python-pip

# install python libraries
RUN pip install mock-server tornado==4.2

# create directory
RUN mkdir -p api

EXPOSE 8888

CMD ["mock-server","--address=0.0.0.0","--dir=api"]

Create Docker image and container (inside VM)

# create Docker image
$ docker build -t api_image .

# list Docker image(s)
$ docker images

# create and start new container from Docker image
$ docker run --name api_container -d -p 8888:8888 api_image

# list Docker container(s)
$ docker ps -a

Run application in browser

Now open a browser and call URL like: http://<192.168.99.100>:8888/__manage. You can now begin to create and use REST API resources.

Create your own REST testing application

This time i want show you how to create your own REST testing application. For this demonstration i use Python, Tkinter and some Python libraries. At the end of this tutorial you can extend the application with more features like “show headers”, “store requests/responses”, “run automatically” and so on.

Preparation

  • install Python > 2.7 (Tkinter included)
  • install Requests: HTTP for Humans
  • install Python Data Validation for Humans
requests==2.8.1
validators==0.9

Example

# -*- coding: utf-8 -*-
from Tkinter import Tk, FALSE


class BaseTkGui(object):
    """Define basic TK GUI"""

    def __init__(self, window_title, window_resizable):
        """Constructor for Tkinter GUI"""

        self._root = Tk()
        self._root.title(str(window_title))

        if not bool(window_resizable):
            self._root.resizable(width=FALSE, height=FALSE)
        else:
            self._root.columnconfigure(0, weight=1)
            self._root.rowconfigure(0, weight=1)

    def start_app(self):
        """Start TK loop"""

        self._root.mainloop()

    def quit_app(self):
        """Stop and quit application"""

        self._root.quit()
# -*- coding: utf-8 -*-
from Tkinter import (Frame, OptionMenu, Entry, Button, StringVar, Label,
                     W, E, NO, END, SOLID)
from ttk import Treeview, Separator
from ScrolledText import ScrolledText
from BaseGui import BaseTkGui


class ApplicationTkGui(BaseTkGui):
    """Define application TK GUI"""

    OPTIONS = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']

    def __init__(self, window_title):
        """Constructor for specific application GUI"""

        BaseTkGui.__init__(self, window_title, False)

        self._method = None
        self._url = None
        self._tree = None
        self._status = None
        self._time = None
        self._key = None
        self._value = None
        self._output = None

    def build_frames(self):
        """Add all frames and start mainloop"""

        self.__top_frame()
        self.__middle_frame()
        self.__bottom_frame()

        self.start_app()

    def prepare_req(self):
        """Overwritten method"""

        pass

    def _add_items(self):
        """Add items into TreeView at end"""

        key = str(self._key.get())
        value = str(self._value.get())

        if key and value:
            self._tree.insert("", "end", values=(key, value))
            self._key.delete(0, END)
            self._value.delete(0, END)

        self._tree.bind("<Double-1>", self._delete_item)

    def _delete_item(self, event):
        """Delete items from TreeView by ID"""

        item = self._tree.identify_row(event.y)
        self._tree.delete(item)

    def __top_frame(self):
        """Top frame creator"""

        self._method = StringVar(self._root)
        self._method.set("GET")

        frame = Frame(self._root)
        frame.grid(column=0, row=0, sticky=W+E)
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)

        option = OptionMenu(frame, self._method, *self.OPTIONS)
        option.grid(column=0, row=0, padx=5, pady=5)

        self._url = Entry(frame, width=50)
        self._url.grid(column=1, row=0, padx=5, pady=5)
        self._url.configure(borderwidth=1, relief=SOLID)
        self._url.configure(highlightthickness=0)
        self._url.insert(0, 'http://')

        submit = Button(frame, text='Submit', command=self.prepare_req)
        submit.grid(column=3, row=0, padx=5, pady=5)

        Separator(frame).grid(columnspan=4, row=1, sticky=W+E)

    def __middle_frame(self):
        """Middle frame creator"""

        frame = Frame(self._root)
        frame.grid(column=0, row=1, sticky=W+E)
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)

        self._tree = Treeview(frame, columns=("Key", "Val"), selectmode='none')
        self._tree.grid(columnspan=5, row=0, padx=5, pady=5, sticky=W+E)
        self._tree.column('#0', stretch=NO, minwidth=0, width=0)
        self._tree.heading('#1', text='Key')
        self._tree.heading('#2', text='Value')
        self._tree.configure(height=5)

        key = Label(frame, text='Key:')
        key.grid(column=0, row=1, padx=5, pady=5, sticky=E)
        self._key = Entry(frame)
        self._key.grid(column=1, row=1, padx=5, pady=5, sticky=W)
        self._key.configure(borderwidth=1, relief=SOLID)
        self._key.configure(highlightthickness=0)

        value = Label(frame, text='Value:')
        value.grid(column=2, row=1, padx=5, pady=5, sticky=E)
        self._value = Entry(frame)
        self._value.grid(column=3, row=1, padx=5, pady=5, sticky=W)
        self._value.configure(borderwidth=1, relief=SOLID)
        self._value.configure(highlightthickness=0)

        add = Button(frame, text='Add new header', command=self._add_items)
        add.grid(column=4, row=1, padx=5, pady=5)

        Separator(frame).grid(columnspan=5, row=2, sticky=W+E)

    def __bottom_frame(self):
        """Bottom frame creator"""
        font_style = ('verdana', 10, 'normal')

        frame = Frame(self._root)
        frame.grid(column=0, row=2, sticky=W+E)
        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)

        self._status = Label(frame, text='Response Status: -')
        self._status.grid(column=0, row=0, sticky=W)
        self._status.configure(fg='red', font=font_style)

        self._time = Label(frame, text='Time: - ms')
        self._time.grid(column=1, row=0, sticky=E)
        self._time.configure(fg='red', font=font_style)

        self._output = ScrolledText(frame)
        self._output.grid(columnspan=2, row=1, padx=5, pady=5, sticky=W+E)
        self._output.configure(height=12, borderwidth=1, relief=SOLID)
        self._output.configure(highlightthickness=0)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import validators
from Tkinter import END
from ApplicationGui import ApplicationTkGui


class Application(ApplicationTkGui):
    """Define application behaviors"""

    def __init__(self, window_title):
        """Constructor for application with title"""

        ApplicationTkGui.__init__(self, window_title)

    def run_application(self):
        """Start application"""

        self.build_frames()

    def prepare_req(self):
        """Prepare and validate request"""

        headers = dict()
        method = self._method.get()
        url = self._url.get()
        tree_ids = self._tree.get_children()

        for i in tree_ids:
            row = self._tree.item(i)
            add = row['values']
            headers[add[0]] = add[1]

        if validators.url(url):
            self.__do_request(method, url, headers)

    def __do_request(self, method, url, headers):
        """Do request and print results"""

        self._output.delete(1.0, END)

        ses = requests.Session()
        prepped = requests.Request(method, url, headers).prepare()
        response = ses.send(prepped, verify=False, allow_redirects=True)

        code = response.status_code
        body = response.text
        time_delta = response.elapsed
        duration = time_delta.total_seconds()

        status = 'Response Status: ' + str(code)
        time = 'Time: ' + str(duration) + ' ms'
        text = body[0:900] + ' ...'

        self._status.configure(text=status)
        self._time.configure(text=time)
        self._output.insert(END, text)

if __name__ == '__main__':

    RUN = Application('HTTPTester')
    RUN.run_application()

Run application

# set access permissions
$ chmod u+x ./RESTme.py
 
# start application
$ python -B ./RESTme.py

The application should look like:

RESTme Application

Create screenshot with Python Selenium Webdriver

The following example show how easy you could make screenshots with Python Selenium Webdriver.

Precondition

Example

# -*- coding: utf-8 -*-

import unittest
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

from MyLibrary import MyLibrary


class Example(unittest.TestCase):
    """Example class for screen shot"""

    def setUp(self):
        self.driver = webdriver.Firefox()
        self.driver.implicitly_wait(1)
        self.driver.get('http://softwaretester.info')

    def test_something(self):

        elem_id = 'is_not_there'

        try:
            elem = self.driver.find_element_by_id(elem_id)
            elem.click()
        except NoSuchElementException:
            MyLibrary.save_screenshot_picture(self.driver, elem_id)
            raise

    def tearDown(self):
        self.driver.close()

if __name__ == '__main__':
    unittest.main(verbosity=1)

The MyLibrary.py could be used on different places.

# -*- coding: utf-8 -*-

import os
import datetime
import time


class MyLibrary(object):
    """Simple screenshot class"""

    @staticmethod
    def get_date_time():
        """Return date_time"""
        dt_format = '%Y%m%d_%H%M%S'
        return datetime.datetime.fromtimestamp(time.time()).strftime(dt_format)

    @staticmethod
    def save_screenshot_picture(driver, file_name):
        """Make screenshot and save"""

        date_time = MyLibrary.get_date_time()
        current_location = os.getcwd()
        screenshot_folder = current_location + '/screenshot/'

        if not os.path.exists(screenshot_folder):
            os.mkdir(screenshot_folder)

        picture = screenshot_folder + file_name + ' ' + date_time + '.png'
        driver.save_screenshot(picture)

After running the test you should see the folder “/screenshot” with the picture.

DDT with Python Selenium

DDT (Data-driven Testing) with Python Selenium Webdriver is very easy! DDT becomes very useful if you have test cases that contains the same test steps. All values could outsourced into files or databases. This tutorial use CSV files.

Precondition

  • Python installed
  • selenium and ddt library installed

Example

The folder structure for this tutorial looks like:

├── data
│   └── scenario_a.csv
├── library
│   ├── GetData.py
│   └── __init__.py
├── scenarios
│   ├── __init__.py
│   └── scenario_a.py
└── testsuite.py

Into folder “data” we store the csv files. The packages “library” include a function to read the specific csv files and the package “scenarios” include the test cases. The test suite is on root folder.

#!/usr/bin/env python
# -*- coding: utf8 -*-

import unittest

from scenarios.scenario_a import TestScenarioA


# load test cases
scenario_a = unittest.TestLoader().loadTestsFromTestCase(TestScenarioA)

# create test suite
test_suite = unittest.TestSuite([scenario_a])

# execute test suite
unittest.TextTestRunner(verbosity=2).run(test_suite)

Into the “testsuite.py” we add all test cases provided by scenario package.

data folder

target_url,elem_name,search_value
http://softwaretester.info,s,python
http://softwaretester.info,s,selenium
http://softwaretester.info,s,webdriver
http://softwaretester.info,s,automation

The CSV stores the test data that we supplied to the @data decorator of test case.

library package

#!/usr/bin/env python
# -*- coding: utf8 -*-

__author__ = 'lupin'
#!/usr/bin/env python
# -*- coding: utf8 -*-

import csv


def get_csv_data(csv_path):
    """
    read test data from csv and return as list

    @type csv_path: string
    @param csv_path: some csv path string
    @return list
    """
    rows = []
    csv_data = open(str(csv_path), "rb")
    content = csv.reader(csv_data)

    # skip header line
    next(content, None)

    # add rows to list
    for row in content:
        rows.append(row)

    return rows

Just for read the csv and return the values as a list.

scenarios package

#!/usr/bin/env python
# -*- coding: utf8 -*-

__author__ = 'lupin'
#!/usr/bin/env python
# -*- coding: utf8 -*-

import unittest

from selenium import webdriver
from ddt import ddt, data, unpack

from library.GetData import get_csv_data


@ddt
class TestScenarioA(unittest.TestCase):
    """ inheriting the TestCase class"""

    @classmethod
    def setUpClass(cls):
        """test preparation"""
        cls.driver = webdriver.Firefox()
        cls.driver.implicitly_wait(3)
        cls.driver.set_window_size(450, 500)

    @data(*get_csv_data('./data/scenario_a.csv'))
    @unpack
    def test_search(self, target_url, elem_name, search_value):
        """test case for scenario a"""
        driver = self.driver
        driver.get(target_url)

        btn_elem = driver.find_element_by_id('search-toggle')
        btn_elem.click()

        input_elem = driver.find_element_by_name(elem_name)
        input_elem.clear()
        input_elem.send_keys(search_value)
        input_elem.submit()

    @classmethod
    def tearDownClass(cls):
        """clean up"""
        cls.driver.close()

Test case with @ddt (for classes), @data and @unpack (for methods) decorators.

  • @data take the arguments from csv file
  • @unpack unpacks tuples or lists into multiple arguments

The test_search() method accepts the arguments, which will be mapped to the tuple values by ddt.

Run

$ python -B testsuite.py

Python, Selenium and PhantomJS – ignore certificate errors

Background

You are done with your work and push all into Git. The Build-Server starts his work and all test scripts are failing. Short look and it is clear – certificate errors. The next example shows, how to ignore certificate errors on PhantomJS.

@classmethod
def setUpClass(cls):
    """starting point for test cases"""

    cls.driver = webdriver.PhantomJS(service_args=['--ignore-ssl-errors=true'])
    cls.driver.implicitly_wait(30)
    cls.driver.set_window_size(1024, 768)
    cls.driver.get(cls.url)

Now it should work….

Startup with Jasmine

Jasmine is a free JavaScript testing framework for BDD (Behavior Driven Development). In my opinion Unit-test should be written by developers, but tester you need also to know! This tutorial give you a hint how to use it.

Preparation

Download latest standalone version directly from GitHub and unzip. Now you have some files and folders. The file “SpecRunner.html” and “spec”, “src” folders are important for us. Some examples are allready  included. If you run the “SpecRunner.html” in your browser, you can see the first example results. A look inside this examples help you understand Jasmine!

Short Introduction

In the “src” directory, are the things to be tested. The “spec” directory has the tests. The “SpecRunner.html” link to the all files. The comments describe what you should do. Whenever you want to run the tests, you simply need to load or reload.

Example

Delete all files into “src” and “spec” folders. Create on “src” folder a new file.

function say_hello(name) {
    return "hello from " + name;
}
function simple_calc(value_a, value_b) {
    return value_a + value_b;
}
function return_bool(value_a) {
    return isNaN(value_a);
}
var timo = {human: "male"};
var john = {human: "male"};

And on “spec” folder this file.

describe("my first test suite", function() {
    it("test for correct string", function() {
        expect(say_hello("Lisa")).toContain("hello from Lisa");
    });
    it("test for correct value", function() {
        expect(simple_calc(5, 6)).toEqual(11);
    });
    it("test for not a number", function() {
        expect(return_bool("Hello")).toBeTruthy();
    });
    it("test for same object", function() {
        expect(timo).toBe(john);
    });
});

Now edit the “SpecRunner.html”. Just add the paths for JS files.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner v2.3.4</title>
  <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.3.4/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-2.3.4/jasmine.css">
  <script src="lib/jasmine-2.3.4/jasmine.js"></script>
  <script src="lib/jasmine-2.3.4/jasmine-html.js"></script>
  <script src="lib/jasmine-2.3.4/boot.js"></script>
  <!-- include source files here... -->
  <script src="src/example.js"></script>
  <!-- include spec files here... -->
  <script src="spec/example.spec.js"></script>
</head>
<body></body>
</html>

That is it! 😉 Now your results shows 4 Specs with 1 failure.

UI testing with DalekJS and PhantomJS on CentOS

With DalekJS you can automate your functional GUI tests very easy. This article describe how to prepare the test environment on CentOS.

Setup

First, the necessary packages are installed.

# install EPEL (Extra Packages for Enterprise Linux)
$ yum install -y epel-release

# check repositories
$ yum repolist

# update all
$ yum -y update

# install packages
$ yum install -y vim nodejs npm bzip2 freetype fontconfig

# check versions of nodejs and npm
$ node --version
$ npm --version

# change to tmp folder
$ cd /tmp/

# download PhantomJS and unzip
$ curl -O https://phantomjs.googlecode.com/files/phantomjs-1.9.2-linux-x86_64.tar.bz2
$ tar xvf phantomjs-1.9.2-linux-x86_64.tar.bz2

# copy binary, delete unneeded files and check version
$ cp phantomjs-1.9.2-linux-x86_64/bin/phantomjs /usr/local/bin/
$ rm -fr phantomjs-1.9.2-linux-x86_64
$ rm -f phantomjs-1.9.2-linux-x86_64.tar.bz2
$ phantomjs --version

# install dalek-cli (global) and check version
$ npm install dalek-cli -g
$ dalek --version

It is also possible to compile PhantomJS itself, but this takes a lot of time.

Prepare test project

Once all is installed without any issues you can start to create the test project.

# change to home folder
$ cd ~/

# create project folder and change into
$ mkdir DalekTest && cd DalekTest

# create package.json interactively
$ npm init

# install dalekjs on project
$ npm install dalekjs --save-dev

Create test case

Now it is time for the first Test Case. I have used the example from Dalek website.

# create test case file
$ touch test_title.js

# insert content
$ vim test_title.js
module.exports = {'Page title is correct': function (test) {
  test
    .open('http://google.com')
    .assert.title().is('Google', 'It has title')
    .done();
  }
};

Run test

By default DalekJS use PhantomJS as browser. For running the test case simple use dalek command and as argument the test case file (*.js).

# run test case
$ dalek test_title.js