Slack is a really popular instant messenger among our clients. It has very nice integration capabilities (Slack bots) that we use with our CI setup. At Qxf2, we sometimes encourage (and enable!) developers to run automated GUI and API tests against their local builds. So, we decided to enhance our GUI automation framework to post the test result of a local run on Slack. Our framework uses pytest as our test runner. This post shows you how to post pytest’s test results on Slack.
Steps to post the test reports on the Slack:
To post reports on the Slack channel, you need to follow the below 5 steps:
- Setup Slack Incoming WebHooks
- Write script to post the test report/any message on the Slack channel
- Use pytest plugin hook to call Slack integration
- Put it all together
- Run the test
Step-1 Setup Slack Incoming WebHooks:
To generate Slack incoming webhook URL, you need to do following steps:
- With help of the Slack link available here, login and navigate to custom integration option shown in Fig. 1.
- From custom integration option list, select ‘Incoming WebHooks’ option and select a channel or create a new channel to post the test reports/message.
- After choosing a channel, click on ‘Add Incoming WebHooks integration’ Button which will generate Incoming WebHook URL as shown in Fig 3. And also allow a user to edit details and add the custom icon.
- Now copy/note down the generated Incoming WebHook URL and save settings. Mostly Incoming WebHook URL has the following format: https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXX We need this URL to post the test reports/any message on Slack channel.
Step-2 Write script to post the test report on Slack:
To post the test report or any message on the Slack channel, we require slack incoming webhook URL which we generated in the previous step and a test report in .txt or .log format. To generate a test report in .txt or .log format, we need to use > test_report.log
argument along with the pytest command:
py.test -v > pytest_report.log |
Once you get both slack incoming webhook and test report in .log or .txt format, you can post the test report on the slack channel using the following script/code.
#Contents of post_test_reports_to_slack.py import json,os,requests def post_reports_to_slack(): url= "Put your Slack incoming webhook url here" #To generate report file add "> pytest_report.log" at end of py.test command for e.g. py.test -v > pytest_report.log test_report_file = os.path.abspath(os.path.join(os.path.dirname(__file__),'pytest_report.log')) #Add report file name and address here # Open report file and read data with open(test_report_file, "r") as in_file: testdata = "" for line in in_file: testdata = testdata + '\n' + line # Set Slack Pass Fail bar indicator color according to test results if 'FAILED' in testdata: bar_color = "#ff0000" else: bar_color = "#36a64f" # Arrange your data in pre-defined format. Test your data format here: https://api.slack.com/docs/messages/builder? data = {"attachments":[ {"color": bar_color, "title": "Test Report", "text": testdata} ]} json_params_encoded = json.dumps(data) slack_response = requests.post(url=url,data=json_params_encoded,headers={"Content-type":"application/json"}) if slack_response.text == 'ok': print '\n Successfully posted pytest report on Slack channel' else: print '\n Something went wrong. Unable to post pytest report on Slack channel. Slack Response:', slack_response #---USAGE EXAMPLES if __name__=='__main__': post_reports_to_slack() |
Step-3 Use pytest plugin hook to call Slack integration:
To get the status of all tests built in a session, we need to call slack integration script after completion of the entire test run. In pytest, there is a provision to run scripts/code after execution of all tests. pytest_sessionfinish
plugin hook executes after the whole test run finishes. So we used pytest_sessionfinish
plugin hook to call Slack integration script. To know more about the pytest plugin hook refer to _pytest.hookspec doc.
To use pytest_sessionfinish
plugin hook, we need to modify conftest.py
with the following code:
#Contents of conftest.py import post_test_reports_to_slack # import slack integration file #Test arguments @pytest.fixture def slack_integration_flag(): "pytest fixture for os version" return pytest.config.getoption("-I") #Command line options: def pytest_addoption(parser): "add parser options" parser.addoption("-I","--slack_integration_flag", dest="slack_integration_flag", default="N", help="Post the test report on slack channel: Y or N") #pytest plugin hook def pytest_sessionfinish(session, exitstatus): "executes after whole test run finishes." if pytest.config.getoption("-I").lower() == 'y': post_test_reports_to_slack.post_reports_to_slack() |
Note: In above code, we added a slack integration flag command line option to enable/disable slack integration.
Step-4 Put it all together:
We are including a sample test for you to check out the pytest and Slack integration. The test script looks like the code below:
#content of test_example_form.py """ Qxf2 Services: Utility script to test example form NOTE: This is a contrived example that was written up to make this blog post clear We do not use this coding pattern at our clients """ from selenium import webdriver import sys,time def test_example_form(browser): "Test example form" #Create an instance of WebDriver if browser.lower() == 'firefox': driver = webdriver.Firefox() elif browser.lower() == 'chrome': driver = webdriver.Chrome() #Create variables to keep count of pass/fail pass_check_counter = 0 total_checks = 0 #Visit the tutorial page driver.get('http://qxf2.com/selenium-tutorial-main') #Check 1: Is the page title correct? if(driver.title=="Qxf2 Services: Selenium training main"): print ("Success: Title of the Qxf2 Tutorial page is correct") pass_check_counter += 1 else: print ("Failed: Qxf2 Tutorial page Title is incorrect") total_checks += 1 #Fill name, email and phone in the example form name_field = driver.find_element_by_xpath("//input[@type='name']") name_field.send_keys('Rohan') email_field = driver.find_element_by_xpath("//input[@type='email']") email_field.send_keys('[email protected]') phone_field = driver.find_element_by_xpath("//input[@type='phone']") phone_field.send_keys('9999999999') submit_button = driver.find_element_by_xpath("//button[@type='submit']") #Click on the Click me button submit_button.click() time.sleep(5) #Check 2: Is the page title correct? if(driver.title=="Qxf2 Services: Selenium training redirect"): print ("Success: The example form was submitted") pass_check_counter += 1 else: print ("Failed: The example form was not submitted. Automation is not on the redirect page") total_checks += 1 #Quit the browser window driver.quit() #Assert if the pass and fail check counters are equal assert total_checks == pass_check_counter #---START OF SCRIPT if __name__=='__main__': browser = sys.argv[1] #Note:using sys.argv to keep this example short. We use OptionParser with all our clients text_example_form(browser) |
The completed conftest.py
looks like this:
#Contents of conftest.py import pytest import os import post_test_reports_to_slack # import slack integration file #Test arguments @pytest.fixture def browser(): "pytest fixture for browser" return pytest.config.getoption("-B") @pytest.fixture def slack_integration_flag(): "pytest fixture for os version" return pytest.config.getoption("-I") #command line options def pytest_addoption(parser): parser.addoption("-B","--browser", dest="browser", default="firefox", help="Browser. Valid options are firefox, ie and chrome") parser.addoption("-I","--slack_integration_flag", dest="slack_integration_flag", default="N", help="Post the test report on slack channel: Y or N") #pytest plugin hook def pytest_sessionfinish(session, exitstatus): "executes after whole test run finishes." if pytest.config.getoption("-I").lower() == 'y': post_test_reports_to_slack.post_reports_to_slack() |
Step-5 Run the test:
To run the test use the command py.test -v -I Y > pytest_report.log
After completion of the test, you receive test report on Slack channel. Look at Fig 4 to see a screenshot of test report notification we received on Slack.
Special Case: xdist and Slack integration
Recently, we come across an issue with use of pytest_sessionfinish
plugin hook. When we ran our tests in parallel (using pytest-xdist), we received multiple messages on the Slack channel. This is because when we run tests in parallel, tests get distributed across multiple CPU/subprocesses which call pytest_sessionfinish
plugin hook multiple times.
To solve this problem, we used pytest_terminal_summary
plugin hook instead of using pytest_sessionfinish
plugin hook. To use pytest_terminal_summary
plugin hook you need to small modification in conftest.py
. You need to replace pytest_sessionfinish
method with pytest_terminal_summary
method given below.
#Replace complete pytest_sessionfinish method with following method def pytest_terminal_summary(terminalreporter, exitstatus): "add additional section in terminal summary reporting." if pytest.config.getoption("-I").lower() == 'y': post_test_reports_to_slack.post_reports_to_slack() |
If you are a startup finding it hard to hire technical QA engineers, learn more about Qxf2 Services.
I love technology and learning new things. I explore both hardware and software. I am passionate about robotics and embedded systems which motivate me to develop my software and hardware skills. I have good knowledge of Python, Selenium, Arduino, C and hardware design. I have developed several robots and participated in robotics competitions. I am constantly exploring new test ideas and test tools for software and hardware. At Qxf2, I am working on developing hardware tools for automated tests ala Tapster. Incidentally, I created Qxf2’s first robot. Besides testing, I like playing cricket, badminton and developing embedded gadget for fun.
Thank you for this great article!
From pytest 5.0 the pytest global variable became deprecated.
Instead, it is advised to use the request.config (via the ‘request’ fixture).
From the pytest docs: https://docs.pytest.org/en/latest/deprecations.html#pytest-config-global
So, specifically, for our case, inside the “pytest_sessionfinish(session, exitstatus)” hook we can access the config object through session object.
So, pytest.config.getoption(“-S”) will turn into -> session.config.getoption(“-S”)
and consequently, the whole “slack_integration_flag” fixture can be completely removed from conftest.py(or just remove the pytest.fixture decorator).
Thanks for the great article!
From pytest 5.0 the ‘pytest’ global variable is deprecated.
Instead, it is advised to access the config via the request fixture (request.config). Many hooks can access the config object through different objects, indirectly
pytest docs: https://docs.pytest.org/en/latest/deprecations.html#pytest-config-global
In particular, in our case, inside the pytest_sessionfinish(session, exitstatus), we have to access the config object through the session object.
So, it becomes session.config.getoption(“-I”).
Hi, thank you for the suggestions. Will update the article soon.
This has been a great help in integrating slack reporting in the framework; however, even with using pytest_terminal_summary hook, it’s sending multiple reports to Slack at the end of the tests. Am I missing something or is there a workaround for it? My code in conftest.py looks like the following:
def pytest_terminal_summary(terminalreporter, exitstatus):
url = sb_config.data
dir_path = os.path.abspath(os.path.join(os.path.dirname(__file__)))
test_report_file = dir_path + “/tests/” + “pytest_report.log”
with open(test_report_file, “r”) as in_file:
testdata = “”
for line in in_file:
testdata = testdata + ‘\n’ + line
test_status = “”
# if ‘FAILURES’ or ‘ERRORS’ in testdata:
bar_color = “#ff0000”
test_status = “Test(s) Failed, \n see test report for details”
else:
bar_color = “#36a64f”
test_status = “All Tests Passed”
data = {“attachments”: [
{
“color”: bar_color,
“title”: “Test Report for project”,
“text”: test_status
}
]}
json_params_encoded = json.dumps(data)
slack_response = requests.post(url=url, data=json_params_encoded,
headers={“Content-type”: “application/json”})
if slack_response.text == ‘ok’:
print(‘\n Successfully posted pytest report on Slack Channel’)
else:
print(‘\n Something went wrong, unable to post post pytest test on Slack Channel.’, slack_response)
The code which you have mentioned is actually post_test_reports_to_slack.py, can you cross verify the code of conftest.py within the blog and check again?
Thanks