Python Dynamic Test Plan generation - testing

I am using Sphinx for documentation and pytest for testing.
I need to generate a test plan but I really don't want to generate it by hand.
It occurred to me that a neat solution would be to actually embed test metadata in the tests' themselves, within their respective docstrings. This metadata would include things like % complete, time remaining etc. I could then run through all of the tests (which would at this point include mostly placeholders) and generate a test plan from them. This would then guarantee that the test plan and the tests themselves would be in sync.
I was thinking of making either a pytest plugin or a sphinx plugin to handle this.
Using pytest, the closest hook I can see looks like pytest_collection_modifyitems which gets called after all of the tests are collected.
Alternatively, I was thinking of using Sphinx and perhaps copying/modifying the todolist plugin as it seems like the closest match to this idea. The output of this would be more useful as the output would slot nicely in to the existing Sphinx based docs I have though there is a lot going on in this plugin and I don't really have the time to invest in understanding it.
The docstrings could have something like this within it:
:plan_complete: 50 #% indicator of how complete this test is
:plan_remaining: 2 #the number of hours estimated to complete this test
:plan_focus: something #what is the test focused on testing
The idea is to then generate a simple markdown/rst or similar table based on the function's name, docstring and embedded plan info and use that as the test plan.
Does something like this already exist?

In the end I went with a pytest based plugin as it was just so much simpler to code.
If anyone else is interested, below is the plugin:
"""Module to generate a test plan table based upon metadata extracted from test
docstrings. The test description is extracted from the first sentence or up to
the first blank line. The data which is extracted from the docstrings are of the
format:
:test_remaining: 10 #number of hours remaining for this test to be complete. If
not present, assumed to be 0
:test_complete: #the percentage of the test that is complete. If not
present, assumed to be 100
:test_focus: The item the test is focusing on such as a DLL call.
"""
import pytest
import re
from functools import partial
from operator import itemgetter
from pathlib import Path
whitespace_re = re.compile(r'\s+')
cut_whitespace = partial(whitespace_re.sub, ' ')
plan_re = re.compile(r':plan_(\w+?):')
plan_handlers = {
'remaining': lambda x:int(x.split('#')[0]),
'complete': lambda x:int(x.strip().split('#')[0]),
'focus': lambda x:x.strip().split('#')[0]
}
csv_template = """.. csv-table:: Test Plan
:header: "Name", "Focus", "% Complete", "Hours remaining", "description", "path"
:widths: 20, 20, 10, 10, 60, 100
{tests}
Overall hours remaining: {hours_remaining:.2f}
Overall % complete: {complete:.2f}
"""
class GeneratePlan:
def __init__(self, output_file=Path('test_plan.rst')):
self.output_file = output_file
def pytest_collection_modifyitems(self, session, config, items):
#breakpoint()
items_to_parse = {i.nodeid.split('[')[0]:i for i in self.item_filter(items)}
#parsed = map(parse_item, items_to_parse.items())
parsed = [self.parse_item(n,i) for (n,i) in items_to_parse.items()]
complete, hours_remaining = self.get_summary_data(parsed)
self.output_file.write_text(csv_template.format(
tests = '\n'.join(self.generate_rst_table(parsed)),
complete=complete,
hours_remaining=hours_remaining))
def item_filter(self, items):
return items #override me
def get_summary_data(self, parsed):
completes = [p['complete'] for p in parsed]
overall_complete = sum(completes)/len(completes)
overall_hours_remaining = sum(p['remaining'] for p in parsed)
return overall_complete, overall_hours_remaining
def generate_rst_table(self, items):
"Use CSV type for simplicity"
sorted_items = sorted(items, key=lambda x:x['name'])
quoter = lambda x:'"{}"'.format(x)
getter = itemgetter(*'name focus complete remaining description path'.split())
for item in sorted_items:
yield 3*' ' + ', '.join(map(quoter, getter(item)))
def parse_item(self, path, item):
"Process a pytest provided item"
data = {
'name': item.name.split('[')[0],
'path': path.split('::')[0],
'description': '',
'remaining': 0,
'complete': 100,
'focus': ''
}
doc = item.function.__doc__
if doc:
desc = self.extract_description(doc)
data['description'] = desc
plan_info = self.extract_info(doc)
data.update(plan_info)
return data
def extract_description(self, doc):
first_sentence = doc.split('\n\n')[0].replace('\n',' ')
return cut_whitespace(first_sentence)
def extract_info(self, doc):
plan_info = {}
for sub_str in doc.split('\n\n'):
cleaned = cut_whitespace(sub_str.replace('\n', ' '))
splitted = plan_re.split(cleaned)
if len(splitted) > 1:
i = iter(splitted[1:]) #splitter starts at index 1
while True:
try:
key = next(i)
val = next(i)
except StopIteration:
break
assert key
if key in plan_handlers:
plan_info[key] = plan_handlers[key](val)
return plan_info
From my conftest.py file, I have a command line argument configured within a pytest_addoption function: parser.addoption('--generate_test_plan', action='store_true', default=False, help="Generate test plan")
And I then configure the plugin within this function:
def pytest_configure(config):
output_test_plan_file = Path('docs/source/test_plan.rst')
class CustomPlan(GeneratePlan):
def item_filter(self, items):
return (i for i in items if 'tests/hw_regression_tests' in i.nodeid)
if config.getoption('generate_test_plan'):
config.pluginmanager.register(CustomPlan(output_file=output_test_plan_file))
#config.pluginmanager.register(GeneratePlan())
Finally, in one of my sphinx documentation source files I then just include the output rst file:
Autogenerated test_plan
=======================
The below test_data is extracted from the individual tests in the suite.
.. include:: test_plan.rst

We have done something similar in our company by using Sphinx-needs and Sphinx-Test-Reports.
Inside a test file we use the docstring to store our test-case incl meta-data:
def my_test():
"""
.. test:: My test case
:id: TEST_001
:status: in progress
:author: me
This test case checks for **awesome** stuff.
"""
a = 2
b = 5
# ToDo: chek if a+b = 7
Then we document the test cases by using autodoc.
My tests
========
.. automodule:: test.my_tests:
:members:
This results in some nice test-case objects in sphinx, which we can filter, link and present in table and flowcharts. See Sphinx-Needs.
With Sphinx-Test-Reports we are loading the results into the docs as well:
.. test-report: My Test report
:id: REPORT_1
:file: ../pytest_junit_results.xml
:links: [[tr_link('case_name', 'signature')]]
This will create objects for each test case, which we also can filter and link.
Thanks of tr_link the result objects get automatically linked to the test case objects.
After that we have all needed information in sphinx and can use e.g. .. needtable:: to get custom views on it.

Related

Calling feature file within another feature file using tags [duplicate]

We have a feature A with several scenarios. And we need one scenario from that file. Can we call it in our feature B?
No. You need to extract that Scenario into a separate*.feature file and then re-use it using the call keyword.
EDIT: Karate 0.9.0 onwards will support being able to call by tag as follows:
* def result = call read('some.feature#tagname')
To illustrate the answer from Karate-inventor Peter Thomas with an example:
Given a feature-file some.feature with multiple scenarios tagged by a tag-decorator:
#tagname
Scenario: A, base case that shares results to e.g. B
// given, when, then ... and share result in a map with keyword `uri`
* def uri = responseHeaders['Location'][0]
#anotherTag
Scenario: X, base case that shares results to e.g. B
// given, when, then ... and share result in a map with keyword `createdResourceId`
* def createdResourceId = $.id
In another feature we can call a specific scenario from that feature by its tag, e.g. tagname:
Scenario: B, another case reusing some results of A
* def result = call read('some.feature#tagname')
* print "given result of A is: $result"
Given path result.uri + '/update'
See also: demo of adding custom tags to scenarios

Expand paths from a channel

My data is structured as samples that are run in batches. So I have a directory hierarchy like this:
/path/to/dir/batch_1/sample_1
/path/to/dir/batch_1/sample_2
/path/to/dir/batch_1/...
/path/to/dir/batch_2/sample_1
/path/to/dir/batch_2/sample_2
/path/to/dir/batch_2/...
/path/to/dir/...
I want to apply a process to every sample for a given subset of batches. One approach that works is to generate a channel listing the samples:
path_to_samples= Channel
.fromPath(['/path/to/dir/batch_2/sample_*',
'/path/to/dir/batch_322/sample_*'], type: 'dir' )
process my_process{
input:
path(sample) from path_to_samples
"""
do stuff
"""
}
Now, I'd like to provide the batch names separately, and have the script find the corresponding samples. Something like that:
params.root_dir = '/path/to/dir/'
params.batch_names = Channel.from('batch_2', 'batch_322')
// make samples channel: incorrect
path_to_samples = params.batch_names
.map { params.root_dir + it + 'sample_*' }
.toPath()
process my_process{
input:
path(sample) from path_to_samples
"""
do stuff
"""
}
So, I am thinking incorrectly about channels? Is there a way to "flatten" the sample list through channel operations? Or is the correct approach to make a more complex Groovy closure that will list the files in each batch directory and return it as a tuple or list?
Not sure how you'd like to provide your input batch names, but you could create your list of glob patterns using a simple closure then use them to create your input channel:
params.root_dir = '/path/to/dir'
params.batch_names = /path/to/batch_names.txt'
batch_names = file(params.batch_names)
sample_dirs = batch_names.readLines().collect { "${params.root_dir}/${it}/sample_*" }
samples = Channel.fromPath( sample_dirs, type: 'dir' )
process my_process{
input:
path(sample) from samples
"""
ls -l "${sample}"
"""
}
I would be inclined to just leave the input glob pattern as a param, though. This approach offers the most flexibility, but may not suit your use case:
params.samples = '/path/to/dir/batch_{2,322}/sample_*'
samples = Channel.fromPath( params.samples, type: 'dir' )

Using call read within a js function

I have a slight problem I want to solve. I have the following lines of code which create 2 users which works. However the issue is that, it creates both users with the same Id from the the first line of code:
def myId = call read('classpath:karate/helpers/guid.js')
def users = function(){ karate.call('classpath:v1/api_CreateUser.feature')}
def usersResult = karate.repeat(2, users )
So I want to be able to create multiple users with different Ids. I tried the following:
* def users =
"""
function(){
var myId = null;
if(myId == null)
{
myId = call read('classpath:karate/helpers/guid.js')
}
karate.call('classpath:v1/api_CreateUser.feature');
}
"""
def usersResult = karate.repeat(2, users )
So the idea is to reset the 'myId' variable everytime to null, check if null which will be true, then call the js function which generates the id and assign the result to 'myId' variable.
Then the variable will be used on the karate.call('classpath:v1/api_CreateUser.feature') line.
Unfortunately I'm getting javascript evaluation failed error.
Anyone could help?
Thanks
Be clear about the slight difference when you are in Karate and when you are in JS. For example:
myId = call read('classpath:karate/helpers/guid.js')
This won't work. What you are looking for is:
myId = karate.call('classpath:karate/helpers/guid.js');
I recommend you read and understand the section on Karate Expressions it will save you a lot of trouble later.
When you use a JS function (that works) you should never need to worry about what you are. Just invoke it wherever, and it will "dispense" a new value. For example if you have:
* def time = function(){ return java.lang.System.currentTimeMillis() + '' }
* def first = time()
* def second = time()
Here first and second will always be different. I think now you have all you need. You are trying to access a JS variable defined in Karate from within a function, this depends on when either was initialized and I don't recommend it if you don't know what you are doing. But if you want to access the latest value of a Karate variable, the right way is to use karate.get(varNameAsString).

Can we call a scenario from one feature in another using karate?

We have a feature A with several scenarios. And we need one scenario from that file. Can we call it in our feature B?
No. You need to extract that Scenario into a separate*.feature file and then re-use it using the call keyword.
EDIT: Karate 0.9.0 onwards will support being able to call by tag as follows:
* def result = call read('some.feature#tagname')
To illustrate the answer from Karate-inventor Peter Thomas with an example:
Given a feature-file some.feature with multiple scenarios tagged by a tag-decorator:
#tagname
Scenario: A, base case that shares results to e.g. B
// given, when, then ... and share result in a map with keyword `uri`
* def uri = responseHeaders['Location'][0]
#anotherTag
Scenario: X, base case that shares results to e.g. B
// given, when, then ... and share result in a map with keyword `createdResourceId`
* def createdResourceId = $.id
In another feature we can call a specific scenario from that feature by its tag, e.g. tagname:
Scenario: B, another case reusing some results of A
* def result = call read('some.feature#tagname')
* print "given result of A is: $result"
Given path result.uri + '/update'
See also: demo of adding custom tags to scenarios

Looping in selenium

I recorded one script using Selenium IDE which contain clicking on a link and now i want to add loop to run same script multiple time, for this i am converting script to python but unable to add loop.Please help me in this regards.
Heres some text direct from selenium docs:
Data Driven Testing:
Data Driven Testing refers to using the same test (or tests) multiple times with varying data. These data sets are often from external files i.e. .csv file, text file, or perhaps loaded from a database. Data driven testing is a commonly used test automation technique used to validate an application against many varying input. When the test is designed for varying data, the input data can expand, essentially creating additional tests, without requiring changes to the test code.
# Collection of String values
source = open("input_file.txt", "r")
values = source.readlines()
source.close()
# Execute For loop for each String in the values array
for search in values:
sel.open("/")
sel.type("q", search)
sel.click("btnG")
sel.waitForPageToLoad("30000")
self.failUnless(sel.is_text_present("Results * for " + search))
Hope it helps. More info at: Selenium Documentation
Best Regards,
Paulo Bueno.
Try a loop similar to this example using "for x in range (0,5):" to set the number of times you wish it to iterate.
def test_py2webdriverselenium(self):
for x in range(0,5):
driver = self.driver
driver.get("http://www.bing.com/")
driver.find_element_by_id("sb_form_q").click()
driver.find_element_by_id("sb_form_q").clear()
driver.find_element_by_id("sb_form_q").send_keys("testing software")
driver.find_element_by_id("sb_form_go").click()
driver.find_element_by_link_text("Bing").click()
I tried this for some situations that I have little information:
list = [''' list containing all items ''']
index = 0
while True:
try:
# do what you want with list[index]
index += 1
except:
# index exception occured
break
In java you can do this as below:
# import packages or classes
public class testClassName(){
before test Methods(){
}
#Test
public void testMethod(){
for(int i =0, i<=5, i++){
WebElement element = driver.findElementById("link_ID");
element.click();
waitForPageLoaded(5);
}
}
after Test Method(){
}
}