How to convert Postman API test into JMeter load test

Posman_meets_JMeter

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!

7 thoughts on “How to convert Postman API test into JMeter load test

  1. 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 `’

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s