While working on mobile application API tests we were thinking how to use the same test set for functional tests as well as for load test and stress test. This would be useful as application was rapidly changing and having separate test set for functional and load/stress test was a trouble. As it would require to double efforts for maintaining this test and keep them up to date.
As I already said in one of my previous posts, I am a bit lazy when it comes to manual work. So I pushed an idea to spend a bit more time for the first time and create a converted that will allow you to give Postman environment and tests as input and get jmx file for JMeter as output. This is what I plan to talk about in current post.
So lets start. As usually I will share some code and explain our approach step by step. A link to a script will be at the end of this post.
- Step 1. Decide which gems will be used. We started with Nokogiri and URI
- Step 2. Parse input files. In order to properly work with different environments on Postman side we need to have at least two file as input. Environment file with variables for current environment and tests file. Also we need a file name to output result to. It looks like this in our case:
#!/bin/ruby require 'json' require 'nokogiri' require 'uri' # Parse args postman_file = nil postman_env_file = nil jmeter_file = nil out_file = nil ARGV.each do|a| arg = a.split('=') if arg[0] == '--postman_file' postman_file = arg[1].strip puts "Postman tests file: #{arg[1]}" end if arg[0] == '--postman_env_file' postman_env_file = arg[1].strip puts "Postman environment file: #{arg[1]}" end if arg[0] == '--out_file' out_file = arg[1].strip puts "Postman ouput file: #{arg[1]}" end end if postman_file.nil? p "--postman_file is not set" exit end if postman_env_file.nil? p "--postman_env_file is not set" exit end begin postman_file = File.read(postman_file) rescue p "File #{postman_file} is not found. Make sure you set a correct path" end begin postman_env_file = File.read(postman_env_file) rescue p "File #{postman_env_file} is not found. Make sure you set a correct path" end test_hash = nil env_hash = nil begin test_hash = JSON.parse(postman_file) rescue p "Cannot parse #{postman_file} as json file" end begin env_hash = JSON.parse(postman_env_file) rescue p "Cannot parse #{postman_file} as json file" end
- Step 3. Now when we validated input files and parsed them, time to generate a file a single file that will have tests with variables replaced. So we can use tests with actual values to generate thread group and threads for JMeter.
def collect_vars arr result = [] arr.select{|el| el.match(/postman.setEnvironmentVariable/)}.each do |var| set = var.split("(")[1].split(",") page_values = set[1].gsub(/\);/,'').split('[').select{|e| !e.gsub!("]","").nil?}.map{|d| d.gsub("'","")} result.push({:var_name=>JSON.parse(set[0]), :page_value=>{:element => page_values.last, :index => page_values[-1].match(/d+/) ? page_values[-1] : 0}}) end result.empty? ? nil : result end def generate_json_with_var postman_file, postman_env_file begin env_hash = JSON.parse(postman_env_file) rescue p "Cannot parse #{postman_file} as json file" end variables = {} env_hash["values"].collect{|element| variables[element["key"]] = element["value"] } variables.each do |key, value| postman_file.gsub!(/\{\{#{key}\}\}/, value) end begin test_hash = JSON.parse(postman_file) rescue p "Cannot parse #{postman_file} as json file" end test_hash end def collect_requests input res = [] input['item'].each do |i| i["item"].each do |test| vars = collect_vars test["event"].first["script"]["exec"] if test["event"] test["request"]["vars"] = vars if !vars.nil? res.push(test["request"]) end end res end test_hash = generate_json_with_var postman_file, postman_env_file requests = collect_requests test_hash
- Step 4. As we have a data for each request we need to generate a new jmx file with xml structure for ThreadGroup and RegexExtractor that will create variables in JMeter to make sure that tests will be dynamic and if they written properly, they will remove data after test was completed.
builder = Nokogiri::XML::Builder.new do |xml| xml.jmeterTestPlan("version"=>"1.2", "properties"=>"2.8", "jmeter"=>"2.13 r1665067") { xml.hashTree { xml.TestPlan("guiclass"=>"TestPlanGui", "testclass"=>"TestPlan", "testname"=>"Stress Tests", "enabled"=>"true") { xml.boolProp(false, "name"=>"TestPlan.functional_mode") xml.boolProp(false, "name"=>"TestPlan.serialize_threadgroups") xml.elementProp("name"=>"TestPlan.user_defined_variables", "elementType"=>"Arguments", "guiclass"=>"ArgumentsPanel", "testclass"=>"Arguments", "testname"=>"User Defined Variables", "enabled"=>"true"){ xml.collectionProp("name"=>"Arguments.arguments") } xml.stringProp("name"=>"TestPlan.user_define_classpath") } xml.hashTree { xml.ThreadGroup("guiclass"=>"ThreadGroupGui", "testclass"=>"ThreadGroup", "testname"=>"Juvly API Performance Test", "enabled"=>"true"){ xml.stringProp("continue", "name"=>"TestPlan.serialize_threadgroups") xml.elementProp("name"=>"ThreadGroup.main_controller", "elementType"=>"LoopController", "guiclass"=>"LoopControlPanel", "testclass"=>"LoopController", "testname"=>"Loop Controller", "enabled"=>"true") { xml.boolProp(false, "name"=>"LoopController.continue_forever") xml.stringProp(1, "name"=>"LoopController.loops") } xml.stringProp(1, "name"=>"ThreadGroup.num_threads") xml.stringProp(1, "name"=>"ThreadGroup.ramp_time") xml.boolProp(false, "name"=>"ThreadGroup.scheduler") xml.stringProp("name"=>"ThreadGroup.duration") xml.stringProp("name"=>"ThreadGroup.delay") } xml.hashTree { requests.each do |request| vars = request['url'].split('/').select{|p| p.match(/{{.+}}/)}.map{|v| v.gsub(/{{|}}/,'')} vars.each do |v| request['url'].gsub!(/{{|}}/, '') end uri = URI(request['url']) vars.each do |v| uri.path.gsub!(/#{v}/, "${#{v}}") end xml.HTTPSamplerProxy("guiclass"=>"HttpTestSampleGui", "testclass"=>"HTTPSamplerProxy", "testname"=>"#{request['url']}", "enabled"=>"true"){ xml.elementProp("name"=>"HTTPsampler.Arguments", "elementType"=>"Arguments", "guiclass"=>"HTTPArgumentsPanel", "testclass"=>"Arguments", "enabled"=>"true"){ if(!request['body'].empty?) xml.collectionProp("name"=>"Arguments.arguments"){ request['body']['formdata'].each do |formelement| xml.elementProp("name"=>"#{formelement['key']}", "elementType"=>"HTTPArgument"){ xml.boolProp(false, "name"=>"HTTPArgument.always_encode") xml.stringProp(formelement['key'], "name"=>"Argument.name") xml.stringProp(formelement['value'], "name"=>"Argument.value") xml.stringProp("=", "name"=>"Argument.metadata") xml.boolProp(true, "name"=>"HTTPArgument.use_equals") } end } end } xml.stringProp("#{uri.host}", "name"=>"HTTPSampler.domain") xml.stringProp("#{uri.port}", "name"=>"HTTPSampler.port") xml.stringProp("name"=>"HTTPSampler.connect_timeout") xml.stringProp("name"=>"HTTPSampler.response_timeout") xml.stringProp("#{uri.scheme}", "name"=>"HTTPSampler.protocol") xml.stringProp("name"=>"HTTPSampler.contentEncoding") xml.stringProp("#{uri.path}", "name"=>"HTTPSampler.path") xml.stringProp("#{request['method']}","name"=>"HTTPSampler.method") xml.boolProp(true,"name"=>"HTTPSampler.follow_redirects") xml.boolProp(false,"name"=>"HTTPSampler.auto_redirects") xml.boolProp(true,"name"=>"HTTPSampler.use_keepalive") xml.boolProp(true,"name"=>"HTTPSampler.DO_MULTIPART_POST") xml.boolProp(true,"name"=>"HTTPSampler.BROWSER_COMPATIBLE_MULTIPART") xml.boolProp(false,"name"=>"HTTPSampler.monitor") xml.stringProp("name"=>"HTTPSampler.embedded_url_re") } xml.hashTree{ xml.HeaderManager("guiclass"=>"HeaderPanel", "testclass"=>"HeaderManager", "testname"=>"HTTP Header Manager", "enabled"=>"true"){ xml.collectionProp("name"=>"HeaderManager.headers"){ request['header'].each do |header| xml.elementProp("name"=>"#{header['key']}", "elementType"=>"Header"){ xml.stringProp("#{header['key']}","name"=>"Header.name") xml.stringProp("#{header['value']}","name"=>"Header.value") } end } } if !request["vars"].nil? xml.hashTree request["vars"].each do |var| xml.RegexExtractor("guiclass"=>"RegexExtractorGui", "testclass"=>"RegexExtractor", "testname"=>"Regular Expression Extractor", "enabled"=>"true"){ xml.stringProp(false,"name"=>"RegexExtractor.useHeaders") xml.stringProp(var[:var_name],"name"=>"RegexExtractor.refname") xml.stringProp("\"#{var[:page_value][:element]}\":\"(.+?)\"","name"=>"RegexExtractor.regex") xml.stringProp("$1$","name"=>"RegexExtractor.template") xml.stringProp(0,"name"=>"RegexExtractor.default") xml.stringProp(1,"name"=>"RegexExtractor.match_number") } xml.hashTree end end } end } } } } end
- Finally we generate output jmx file that we can use for JMeter
outFile = File.new(out_file, "w+") outFile.puts builder.to_xml outFile.close
So now we have a file that you need to open in JMeter UI, configure number of threads that you want to execute and you are good to go.
As promised here is a link to Scimus repository, where we upload examples and tools that we use for testing. If you can add something on fix, some parts of a code you are welcomed there.
Thank you for attention
and hope it was useful
for someone out there!
do you have a sample of the command you would run in terminal?
LikeLike
Hello Dan, it is pretty simple :
./converter.rb --postman_file="postman collection file" --postman_env_file="postman env file" --out_file="output file"
Here is a repo in github with all source : https://github.com/theScimus/postman_to_jmeter_converter
LikeLike
thanks for your quick reply note too sure what I’m doing wrong but I’ve saved my postman collection with the env file and I’m getting the following error.
Postman tests file: api.json
Postman environment file: global.json
converter.rb:85:in `generate_json_with_var’: undefined method `collect’ for nil:NilClass (NoMethodError)
from converter.rb:110:in `’
LikeLike
Let me check this one for you
LikeLike
It looks like you do not have any variables in your postman env file. Can you share environment file with me if possible?
LikeLike
You can join our slack channel if you want, this way it will faster to communicate then from here. Link : https://join.slack.com/t/thescimus/shared_invite/enQtMjQ5MDg4NDc1MTkwLWFjMGNjODY4YjVkYzU5MDRlMWY3OGRkMDQwNDJmYzg1NjllNWZjNDQ5YTE0MDIwOWE0Nzk3MjI4MjExOWM0ZDE
LikeLike
Hi
I have few questions on the Jmeter & Postman collection implementation, So would like to be a part of the Jmeter + Postman slack channel, can i get a link please?
Thanks
shankar
LikeLike