Repository: httprunner/httprunner (language: python)

Issue:
测试用例的 step name 支持参数化
## 期望的功能特性

step name 可以使用 $ 引用参数驱动设置的参数


hrp 性能测试 Statistics Summary 统计结果不准
## 问题描述

如果测试用例中存在引用其他用例的情况下，统计会将整个引用的testcase作为一个request，存在较大统计误差。

## 版本信息

请提供如下版本信息：

 - hrp全版本

## 项目文件内容（非必须）

```
Current time: 2022/06/21 10:48:23, Users: 1, State: quitting, Total RPS: 0.7, Total Average Response Time: 1252.5ms, Total Fail Ratio: 0.0%
Accumulated Transactions: 1 Passed, 0 Failed
+--------------+------------------------+------------+---------+--------+---------+------+------+--------------+------------+-------------+
|     TYPE     |          NAME          | # REQUESTS | # FAILS | MEDIAN | AVERAGE | MIN  | MAX  | CONTENT SIZE | # REQS/SEC | # FAILS/SEC |
+--------------+------------------------+------------+---------+--------+---------+------+------+--------------+------------+-------------+
| request-POST | post form data         |          1 |       0 |    280 |  278.00 |  278 |  278 |          422 |       0.33 |        0.00 |
| testcase     | request with functions |          1 |       0 |   2200 | 2227.00 | 2227 | 2227 |            0 |       0.33 |        0.00 |
| transaction  | Action                 |          1 |       0 |   2800 | 2830.00 | 2830 | 2830 |            0 |       0.33 |        0.00 |
+--------------+------------------------+------------+---------+--------+---------+------+------+--------------+------------+-------------+

=========================================== Statistics Summary ==========================================
Current time: 2022/06/21 10:48:23, Users: 1, Duration: 2.83s, Accumulated Transactions: 1 Passed, 0 Failed
+-------+------------+---------+--------+---------+-----+------+--------------+------------+-------------+
| NAME  | # REQUESTS | # FAILS | MEDIAN | AVERAGE | MIN | MAX  | CONTENT SIZE | # REQS/SEC | # FAILS/SEC |
+-------+------------+---------+--------+---------+-----+------+--------------+------------+-------------+
| Total |          2 |       0 |    280 | 1252.50 | 278 | 2227 |          211 |       0.71 |        0.00 |
+-------+------------+---------+--------+---------+-----+------+--------------+------------+-------------+
```



Discussion / hints:

Fix patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 74bef791b..f72658df2 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Release History
 
+## v4.1.6 (2022-07-04)
+
+**go version**
+
+- fix: step name with parameterize mechanism
+- fix: error of concurrent map writes occurred while uploading in boom mode
+- fix: record all requests of testcase reference in boom mode
+- fix: failed to record the step of occurring error in the html report
+
 ## v4.1.5 (2022-06-27)
 
 **go version**
diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md
index 4273be354..7dc86997e 100644
--- a/docs/cmd/hrp.md
+++ b/docs/cmd/hrp.md
@@ -37,4 +37,4 @@ Copyright 2017 debugtalk
 * [hrp startproject](hrp_startproject.md)	 - create a scaffold project
 * [hrp wiki](hrp_wiki.md)	 - visit https://httprunner.com
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md
index 40cd994d7..7354315a8 100644
--- a/docs/cmd/hrp_boom.md
+++ b/docs/cmd/hrp_boom.md
@@ -42,4 +42,4 @@ hrp boom [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md
index d8b6c97bb..8103dbbd9 100644
--- a/docs/cmd/hrp_build.md
+++ b/docs/cmd/hrp_build.md
@@ -28,4 +28,4 @@ hrp build $path ... [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md
index 756840679..d438da68e 100644
--- a/docs/cmd/hrp_convert.md
+++ b/docs/cmd/hrp_convert.md
@@ -22,4 +22,4 @@ hrp convert $path... [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md
index 3b93507e6..428e69843 100644
--- a/docs/cmd/hrp_run.md
+++ b/docs/cmd/hrp_run.md
@@ -35,4 +35,4 @@ hrp run $path... [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md
index acfcb432c..495440630 100644
--- a/docs/cmd/hrp_startproject.md
+++ b/docs/cmd/hrp_startproject.md
@@ -21,4 +21,4 @@ hrp startproject $project_name [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md
index 7614dbe6c..219920fec 100644
--- a/docs/cmd/hrp_wiki.md
+++ b/docs/cmd/hrp_wiki.md
@@ -16,4 +16,4 @@ hrp wiki [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/examples/demo-empty-project/proj.json b/examples/demo-empty-project/proj.json
index 7405d8a29..fe59965dc 100644
--- a/examples/demo-empty-project/proj.json
+++ b/examples/demo-empty-project/proj.json
@@ -1,5 +1,5 @@
 {
     "project_name": "demo-empty-project",
-    "create_time": "2022-06-27T16:52:45.660111+08:00",
-    "hrp_version": "v4.1.4"
+    "create_time": "2022-07-04T14:54:33.795693+08:00",
+    "hrp_version": "v4.1.5"
 }
diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json
index 33679c33a..08ee10702 100644
--- a/examples/demo-with-go-plugin/proj.json
+++ b/examples/demo-with-go-plugin/proj.json
@@ -1,5 +1,5 @@
 {
     "project_name": "demo-with-go-plugin",
-    "create_time": "2022-06-27T16:51:52.779837+08:00",
-    "hrp_version": "v4.1.4"
+    "create_time": "2022-07-04T14:53:59.755944+08:00",
+    "hrp_version": "v4.1.5"
 }
diff --git a/examples/demo-with-py-plugin/.debugtalk_gen.py b/examples/demo-with-py-plugin/.debugtalk_gen.py
index a57fef721..70910180b 100644
--- a/examples/demo-with-py-plugin/.debugtalk_gen.py
+++ b/examples/demo-with-py-plugin/.debugtalk_gen.py
@@ -1,4 +1,4 @@
-# NOTE: Generated By hrp v4.1.4, DO NOT EDIT!
+# NOTE: Generated By hrp v4.1.5, DO NOT EDIT!
 
 import sys
 import os
diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json
index fca33a09c..6c789922e 100644
--- a/examples/demo-with-py-plugin/proj.json
+++ b/examples/demo-with-py-plugin/proj.json
@@ -1,5 +1,5 @@
 {
     "project_name": "demo-with-py-plugin",
-    "create_time": "2022-06-27T16:51:54.32061+08:00",
-    "hrp_version": "v4.1.4"
+    "create_time": "2022-07-04T14:54:00.346082+08:00",
+    "hrp_version": "v4.1.5"
 }
diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json
index 29065d5b7..24b61c189 100644
--- a/examples/demo-without-plugin/proj.json
+++ b/examples/demo-without-plugin/proj.json
@@ -1,5 +1,5 @@
 {
     "project_name": "demo-without-plugin",
-    "create_time": "2022-06-27T16:52:45.363472+08:00",
-    "hrp_version": "v4.1.4"
+    "create_time": "2022-07-04T14:54:33.495643+08:00",
+    "hrp_version": "v4.1.5"
 }
diff --git a/hrp/boomer.go b/hrp/boomer.go
index 839ebd151..4c2bcebef 100644
--- a/hrp/boomer.go
+++ b/hrp/boomer.go
@@ -125,18 +125,38 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
 
 			startTime := time.Now()
 			for _, step := range testcase.TestSteps {
+				// parse step name
+				parsedName, err := sessionRunner.parser.ParseString(step.Name(), sessionRunner.sessionVariables)
+				if err != nil {
+					parsedName = step.Name()
+				}
+				stepName := convertString(parsedName)
 				// reset start time only once before step
 				once.Do(func() {
 					b.Boomer.ResetStartTime()
 				})
 				stepResult, err := step.Run(sessionRunner)
+				// update step result name with parsed step name
+				stepResult.Name = stepName
+				// record requests result of the step if step type is testcase
+				if stepResult.StepType == stepTypeTestCase && stepResult.Data != nil {
+					// record requests of testcase step
+					for _, result := range stepResult.Data.([]*StepResult) {
+						if result.Success {
+							b.RecordSuccess(string(result.StepType), result.Name, result.Elapsed, result.ContentSize)
+						} else {
+							b.RecordFailure(string(result.StepType), result.Name, result.Elapsed, result.Attachment)
+						}
+					}
+				}
+				// record step failure
 				if err != nil {
 					// step failed
 					var elapsed int64
 					if stepResult != nil {
 						elapsed = stepResult.Elapsed
 					}
-					b.RecordFailure(string(step.Type()), step.Name(), elapsed, err.Error())
+					b.RecordFailure(string(step.Type()), stepResult.Name, elapsed, err.Error())
 
 					// update flag
 					testcaseSuccess = false
@@ -150,7 +170,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
 					continue
 				}
 
-				// step success
+				// record step success
 				if stepResult.StepType == stepTypeTransaction {
 					// transaction
 					// FIXME: support nested transactions
@@ -165,7 +185,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
 					// no record required
 				} else {
 					// request or testcase step
-					b.RecordSuccess(string(step.Type()), step.Name(), stepResult.Elapsed, stepResult.ContentSize)
+					b.RecordSuccess(string(step.Type()), stepResult.Name, stepResult.Elapsed, stepResult.ContentSize)
 					// update extracted variables
 					for k, v := range stepResult.ExportVars {
 						sessionRunner.sessionVariables[k] = v
diff --git a/hrp/internal/boomer/stats.go b/hrp/internal/boomer/stats.go
index c1bbcfe8e..043246cbe 100644
--- a/hrp/internal/boomer/stats.go
+++ b/hrp/internal/boomer/stats.go
@@ -74,12 +74,16 @@ func (s *requestStats) logTransaction(name string, success bool, responseTime in
 }
 
 func (s *requestStats) logRequest(method, name string, responseTime int64, contentLength int64) {
-	s.total.log(responseTime, contentLength)
+	if method != "testcase" {
+		s.total.log(responseTime, contentLength)
+	}
 	s.get(name, method).log(responseTime, contentLength)
 }
 
 func (s *requestStats) logError(method, name, err string) {
-	s.total.logFailures()
+	if method != "testcase" {
+		s.total.logFailures()
+	}
 	s.get(name, method).logFailures()
 
 	// store error in errors map
diff --git a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py
index a57fef721..70910180b 100644
--- a/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py
+++ b/hrp/internal/scaffold/templates/plugin/.debugtalk_gen.py
@@ -1,4 +1,4 @@
-# NOTE: Generated By hrp v4.1.4, DO NOT EDIT!
+# NOTE: Generated By hrp v4.1.5, DO NOT EDIT!
 
 import sys
 import os
diff --git a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go
index 6e6c9067e..0ee1ae227 100644
--- a/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go
+++ b/hrp/internal/scaffold/templates/plugin/debugtalk_gen.go
@@ -1,4 +1,4 @@
-// NOTE: Generated By hrp v4.1.4, DO NOT EDIT!
+// NOTE: Generated By hrp v4.1.5, DO NOT EDIT!
 package main
 
 import (
diff --git a/hrp/session.go b/hrp/session.go
index eae3b41aa..6a57de06d 100644
--- a/hrp/session.go
+++ b/hrp/session.go
@@ -64,10 +64,17 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) error {
 
 	// run step in sequential order
 	for _, step := range r.testCase.TestSteps {
-		log.Info().Str("step", step.Name()).
+		// parse step name
+		parsedName, err := r.parser.ParseString(step.Name(), r.sessionVariables)
+		if err != nil {
+			parsedName = step.Name()
+		}
+		stepName := convertString(parsedName)
+		log.Info().Str("step", stepName).
 			Str("type", string(step.Type())).Msg("run step start")
 
 		stepResult, err := step.Run(r)
+		stepResult.Name = stepName
 		if err != nil {
 			log.Error().
 				Str("step", stepResult.Name).
diff --git a/hrp/step_api.go b/hrp/step_api.go
index cce14cd02..a2c0bad00 100644
--- a/hrp/step_api.go
+++ b/hrp/step_api.go
@@ -117,6 +117,10 @@ func extendWithAPI(testStep *TStep, overriddenStep *API) {
 	}
 	// merge & override request
 	testStep.Request = overriddenStep.Request
+	// init upload
+	if testStep.Request.Upload != nil {
+		initUpload(testStep)
+	}
 	// merge & override variables
 	testStep.Variables = mergeVariables(testStep.Variables, overriddenStep.Variables)
 	// merge & override extractors
diff --git a/hrp/step_request.go b/hrp/step_request.go
index 29a865788..f39fedfa8 100644
--- a/hrp/step_request.go
+++ b/hrp/step_request.go
@@ -259,24 +259,28 @@ func (r *requestBuilder) prepareBody(stepVariables map[string]interface{}) error
 	return nil
 }
 
-func prepareUpload(parser *Parser, step *TStep) (err error) {
+func initUpload(step *TStep) {
+	if step.Request.Headers == nil {
+		step.Request.Headers = make(map[string]string)
+	}
+	step.Request.Headers["Content-Type"] = "${multipart_content_type($m_encoder)}"
+	step.Request.Body = "$m_encoder"
+}
+
+func prepareUpload(parser *Parser, step *TStep, stepVariables map[string]interface{}) (err error) {
 	if step.Request.Upload == nil {
 		return
 	}
-	step.Request.Upload, err = parser.ParseVariables(step.Request.Upload)
+	uploadMap, err := parser.Parse(step.Request.Upload, stepVariables)
 	if err != nil {
 		return
 	}
-	if step.Variables == nil {
-		step.Variables = make(map[string]interface{})
-	}
-	step.Variables["m_upload"] = step.Request.Upload
-	step.Variables["m_encoder"] = fmt.Sprintf("${multipart_encoder($m_upload)}")
-	if step.Request.Headers == nil {
-		step.Request.Headers = make(map[string]string)
+	stepVariables["m_upload"] = uploadMap
+	mEncoder, err := parser.Parse("${multipart_encoder($m_upload)}", stepVariables)
+	if err != nil {
+		return
 	}
-	step.Request.Headers["Content-Type"] = "${multipart_content_type($m_encoder)}"
-	step.Request.Body = "$m_encoder"
+	stepVariables["m_encoder"] = mEncoder
 	return
 }
 
@@ -293,15 +297,25 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
 		if err != nil {
 			stepResult.Attachment = err.Error()
 		}
+		// update summary
+		r.summary.Records = append(r.summary.Records, stepResult)
+		r.summary.Stat.Total += 1
+		if stepResult.Success {
+			r.summary.Stat.Successes += 1
+		} else {
+			r.summary.Stat.Failures += 1
+			// update summary result to failed
+			r.summary.Success = false
+		}
 	}()
 
-	err = prepareUpload(r.parser, step)
+	// override step variables
+	stepVariables, err := r.MergeStepVariables(step.Variables)
 	if err != nil {
 		return
 	}
 
-	// override step variables
-	stepVariables, err := r.MergeStepVariables(step.Variables)
+	err = prepareUpload(r.parser, step, stepVariables)
 	if err != nil {
 		return
 	}
@@ -435,17 +449,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
 	stepResult.ContentSize = resp.ContentLength
 	stepResult.Data = sessionData
 
-	// update summary
-	r.summary.Records = append(r.summary.Records, stepResult)
-	r.summary.Stat.Total += 1
-	if stepResult.Success {
-		r.summary.Stat.Successes += 1
-	} else {
-		r.summary.Stat.Failures += 1
-		// update summary result to failed
-		r.summary.Success = false
-	}
-
 	return stepResult, err
 }
 
@@ -822,6 +825,8 @@ func (s *StepRequestWithOptionalArgs) WithBody(body interface{}) *StepRequestWit
 
 // WithUpload sets HTTP request body for uploading file(s).
 func (s *StepRequestWithOptionalArgs) WithUpload(upload map[string]interface{}) *StepRequestWithOptionalArgs {
+	// init upload
+	initUpload(s.step)
 	s.step.Request.Upload = upload
 	return s
 }
diff --git a/hrp/step_websocket.go b/hrp/step_websocket.go
index 34cc226ce..0967be1fc 100644
--- a/hrp/step_websocket.go
+++ b/hrp/step_websocket.go
@@ -239,6 +239,16 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er
 		if err != nil {
 			stepResult.Attachment = err.Error()
 		}
+		// update summary
+		r.summary.Records = append(r.summary.Records, stepResult)
+		r.summary.Stat.Total += 1
+		if stepResult.Success {
+			r.summary.Stat.Successes += 1
+		} else {
+			r.summary.Stat.Failures += 1
+			// update summary result to failed
+			r.summary.Success = false
+		}
 	}()
 
 	// override step variables
@@ -385,16 +395,6 @@ func runStepWebSocket(r *SessionRunner, step *TStep) (stepResult *StepResult, er
 		stepResult.Success = true
 	}
 
-	// update summary
-	r.summary.Records = append(r.summary.Records, stepResult)
-	r.summary.Stat.Total += 1
-	if stepResult.Success {
-		r.summary.Stat.Successes += 1
-	} else {
-		r.summary.Stat.Failures += 1
-		// update summary result to failed
-		r.summary.Success = false
-	}
 	return stepResult, nil
 }
 
diff --git a/hrp/summary.go b/hrp/summary.go
index 13e6085ad..b73522cc7 100644
--- a/hrp/summary.go
+++ b/hrp/summary.go
@@ -199,5 +199,6 @@ func newSummary() *TestCaseSummary {
 		Stat:    &TestStepStat{},
 		Time:    &TestCaseTime{},
 		InOut:   &TestCaseInOut{},
+		Records: []*StepResult{},
 	}
 }


Test patch:
diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md
index 5c70465b8..fc51e2723 100644
--- a/docs/cmd/hrp_pytest.md
+++ b/docs/cmd/hrp_pytest.md
@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
 
 * [hrp](hrp.md)	 - Next-Generation API Testing Solution.
 
-###### Auto generated by spf13/cobra on 27-Jun-2022
+###### Auto generated by spf13/cobra on 4-Jul-2022
diff --git a/examples/hrp/parameters_test.json b/examples/hrp/parameters_test.json
index 4af949ed5..1763d755a 100644
--- a/examples/hrp/parameters_test.json
+++ b/examples/hrp/parameters_test.json
@@ -31,7 +31,7 @@
   },
   "teststeps": [
     {
-      "name": "get with params",
+      "name": "get with user: $username",
       "variables": {
         "foo1": "$username",
         "foo2": "$password",
diff --git a/examples/hrp/parameters_test.yaml b/examples/hrp/parameters_test.yaml
index 23fd8c010..b431d4b3b 100644
--- a/examples/hrp/parameters_test.yaml
+++ b/examples/hrp/parameters_test.yaml
@@ -20,7 +20,7 @@ config:
   verify: False
 
 teststeps:
-  - name: get with params
+  - name: "get with user: $username"
     variables:
       foo1: $username
       foo2: $password
diff --git a/hrp/step_testcase.go b/hrp/step_testcase.go
index e75e52e4b..09440e5a6 100644
--- a/hrp/step_testcase.go
+++ b/hrp/step_testcase.go
@@ -44,13 +44,20 @@ func (s *StepTestCaseWithOptionalArgs) Struct() *TStep {
 	return s.step
 }
 
-func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
-	stepResult := &StepResult{
+func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepResult, err error) {
+	stepResult = &StepResult{
 		Name:     s.step.Name,
 		StepType: stepTypeTestCase,
 		Success:  false,
 	}
 
+	defer func() {
+		// update testcase summary
+		if err != nil {
+			stepResult.Attachment = err.Error()
+		}
+	}()
+
 	stepVariables, err := r.MergeStepVariables(s.step.Variables)
 	if err != nil {
 		return stepResult, err
@@ -82,12 +89,10 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
 	start := time.Now()
 	// run referenced testcase with step variables
 	err = sessionRunner.Start(stepVariables)
-	stepResult.Elapsed = time.Since(start).Milliseconds()
-	if err != nil {
-		stepResult.Attachment = err.Error()
-		r.summary.Success = false
-		return stepResult, err
+	if err == nil {
+		stepResult.Success = true
 	}
+	stepResult.Elapsed = time.Since(start).Milliseconds()
 	summary := sessionRunner.GetSummary()
 	// update step names
 	for _, record := range summary.Records {
@@ -96,7 +101,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
 	stepResult.Data = summary.Records
 	// export testcase export variables
 	stepResult.ExportVars = summary.InOut.ExportVars
-	stepResult.Success = true
 
 	// merge testcase summary
 	r.summary.Records = append(r.summary.Records, summary.Records...)
@@ -104,5 +108,5 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
 	r.summary.Stat.Successes += summary.Stat.Successes
 	r.summary.Stat.Failures += summary.Stat.Failures
 
-	return stepResult, nil
+	return stepResult, err
 }
diff --git a/hrp/testcase.go b/hrp/testcase.go
index 6a91939b2..69120497e 100644
--- a/hrp/testcase.go
+++ b/hrp/testcase.go
@@ -144,6 +144,10 @@ func (path *TestCasePath) ToTestCase() (*TestCase, error) {
 				step: step,
 			})
 		} else if step.Request != nil {
+			// init upload
+			if step.Request.Upload != nil {
+				initUpload(step)
+			}
 			testCase.TestSteps = append(testCase.TestSteps, &StepRequestWithOptionalArgs{
 				step: step,
 			})
diff --git a/hrp/tests/upload_test.go b/hrp/tests/upload_test.go
index d9273d178..2897eabee 100644
--- a/hrp/tests/upload_test.go
+++ b/hrp/tests/upload_test.go
@@ -9,12 +9,13 @@ import (
 func TestCaseUploadFile(t *testing.T) {
 	testcase := &hrp.TestCase{
 		Config: hrp.NewConfig("test upload file to httpbin").
-			SetBaseURL("https://httpbin.org"),
+			SetBaseURL("https://httpbin.org").
+			WithVariables(map[string]interface{}{"upload_file": "test.env"}),
 		TestSteps: []hrp.IStep{
 			hrp.NewStep("upload file").
 				WithVariables(map[string]interface{}{
 					"m_encoder": "${multipart_encoder($m_upload)}",
-					"m_upload":  map[string]interface{}{"file": "test.env"},
+					"m_upload":  map[string]interface{}{"file": "$upload_file"},
 				}).
 				POST("/post").
 				WithHeaders(map[string]string{"Content-Type": "${multipart_content_type($m_encoder)}"}).
@@ -24,7 +25,7 @@ func TestCaseUploadFile(t *testing.T) {
 				AssertStartsWith("body.files.file", "UserName=test", "check uploaded file"),
 			hrp.NewStep("upload file with keyword").
 				POST("/post").
-				WithUpload(map[string]interface{}{"file": "test.env"}).
+				WithUpload(map[string]interface{}{"file": "$upload_file"}).
 				Validate().
 				AssertEqual("status_code", 200, "check status code").
 				AssertStartsWith("body.files.file", "UserName=test", "check uploaded file"),