Skip to content

Commit

Permalink
Add Support for Running Specific Tests
Browse files Browse the repository at this point in the history
Update `force test` to allow specific test methods within a class to be
run.
  • Loading branch information
cwarden committed Oct 27, 2017
1 parent e9cd99b commit 133ec86
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 46 deletions.
5 changes: 3 additions & 2 deletions command/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

var cmdTest = &Command{
Usage: "test (all | classname...)",
Usage: "test (all | classname... | classname.method...)",
Short: "Run apex tests",
Long: `
Run apex tests
Expand All @@ -23,7 +23,8 @@ Examples:
force test all
force test Test1 Test2 Test3
force test -namespace=ns Test4
force test Test1.method1 Test1.method2
force test -namespace=ns Test4
force test -v Test1
`,
}
Expand Down
44 changes: 0 additions & 44 deletions lib/partner.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,6 @@ type ForcePartner struct {
Force *Force
}

type TestRunner interface {
RunTests(tests []string, namespace string) (output TestCoverage, err error)
}

type TestCoverage struct {
Log string `xml:"Header>DebuggingInfo>debugLog"`
NumberRun int `xml:"Body>runTestsResponse>result>numTestsRun"`
NumberFailures int `xml:"Body>runTestsResponse>result>numFailures"`
NumberLocations []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocations"`
NumberLocationsNotCovered []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocationsNotCovered"`
Name []string `xml:"Body>runTestsResponse>result>codeCoverage>name"`
SMethodNames []string `xml:"Body>runTestsResponse>result>successes>methodName"`
SClassNames []string `xml:"Body>runTestsResponse>result>successes>name"`
FMethodNames []string `xml:"Body>runTestsResponse>result>failures>methodName"`
FClassNames []string `xml:"Body>runTestsResponse>result>failures>name"`
FMessage []string `xml:"Body>runTestsResponse>result>failures>message"`
FStackTrace []string `xml:"Body>runTestsResponse>result>failures>stackTrace"`
}

func NewForcePartner(force *Force) (partner *ForcePartner) {
partner = &ForcePartner{Force: force}
return
Expand Down Expand Up @@ -131,31 +112,6 @@ func (partner *ForcePartner) SoapExecuteCore(action, query string) (response []b
return
}

func (partner *ForcePartner) RunTests(tests []string, namespace string) (output TestCoverage, err error) {
soap := "<RunTestsRequest>\n"
if strings.EqualFold(tests[0], "all") {
soap += "<allTests>True</allTests>\n"
} else {
for _, element := range tests {
soap += "<classes>" + element + "</classes>\n"
}
}
if namespace != "" {
soap += "<namespace>" + namespace + "</namespace>\n"
}
soap += "</RunTestsRequest>"
body, err := partner.soapExecute("runTests", soap)
if err != nil {
return
}
var result TestCoverage
if err = xml.Unmarshal(body, &result); err != nil {
return
}
output = result
return
}

func (partner *ForcePartner) soapExecute(action, query string) (response []byte, err error) {
url := fmt.Sprintf("%s/services/Soap/s/%s/%s", partner.Force.Credentials.InstanceUrl, partner.Force.Credentials.SessionOptions.ApiVersion, partner.Force.Credentials.UserInfo.OrgId)
soap := NewSoap(url, "http://soap.sforce.com/2006/08/apex", partner.Force.Credentials.AccessToken)
Expand Down
121 changes: 121 additions & 0 deletions lib/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package lib

import (
"encoding/xml"
"errors"
"strings"
)

type TestRunner interface {
RunTests(tests []string, namespace string) (output TestCoverage, err error)
}

type TestCoverage struct {
Log string `xml:"Header>DebuggingInfo>debugLog"`
NumberRun int `xml:"Body>runTestsResponse>result>numTestsRun"`
NumberFailures int `xml:"Body>runTestsResponse>result>numFailures"`
NumberLocations []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocations"`
NumberLocationsNotCovered []int `xml:"Body>runTestsResponse>result>codeCoverage>numLocationsNotCovered"`
Name []string `xml:"Body>runTestsResponse>result>codeCoverage>name"`
SMethodNames []string `xml:"Body>runTestsResponse>result>successes>methodName"`
SClassNames []string `xml:"Body>runTestsResponse>result>successes>name"`
FMethodNames []string `xml:"Body>runTestsResponse>result>failures>methodName"`
FClassNames []string `xml:"Body>runTestsResponse>result>failures>name"`
FMessage []string `xml:"Body>runTestsResponse>result>failures>message"`
FStackTrace []string `xml:"Body>runTestsResponse>result>failures>stackTrace"`
}

type TestNode struct {
ClassName string `xml:"className"`
TestMethods []string `xml:"testMethods"`
}

type RunTestsRequest struct {
AllTests bool `xml:"allTests"`
Classes []string `xml:"classes"`
Namespace string `xml:"namespace"`
MaxFailedTests int `xml:"maxFailedTests"`
Tests []TestNode `xml:"tests,omitEmpty"`
}

func containsMethods(tests []string) (result bool, err error) {
containsMethods := make(map[bool]int)
classNames := make(map[string]int)
for _, v := range tests {
class, method := splitClassMethod(v)
containsMethods[len(method) > 0]++
classNames[class]++
}
if len(classNames) > 1 && (len(containsMethods) > 1 || containsMethods[true] > 0) {
err = errors.New("Tests must all be either class names or methods within the same class")
return
}
_, result = containsMethods[true]
return
}

func splitClassMethod(s string) (string, string) {
if len(s) == 0 {
return s, s
}
slice := strings.SplitN(s, ".", 2)
if len(slice) == 1 {
return slice[0], ""
}
return slice[0], slice[1]
}

func NewRunTestsRequest(tests []string, namespace string) (request RunTestsRequest, err error) {
request = RunTestsRequest{
MaxFailedTests: -1,
Namespace: namespace,
}
if len(tests) == 0 || (len(tests) == 1 && strings.EqualFold(tests[0], "all")) {
request.AllTests = true
return
}

containsMethods, err := containsMethods(tests)
if err != nil {
return
}
if !containsMethods {
request.Classes = tests
} else {
var methods []string
var class string
for _, v := range tests {
var method string
class, method = splitClassMethod(v)
methods = append(methods, method)
}
// Per the docs, the list of TestNodes can only contain one element
request.Tests = make([]TestNode, 1, 1)
request.Tests[0] = TestNode{
ClassName: class,
TestMethods: methods,
}
}
return
}

func (partner *ForcePartner) RunTests(tests []string, namespace string) (output TestCoverage, err error) {
request, err := NewRunTestsRequest(tests, namespace)
if err != nil {
return
}
soap, err := xml.MarshalIndent(request, " ", " ")
if err != nil {
return
}
body, err := partner.soapExecute("runTests", string(soap))
if err != nil {
return
}
var result TestCoverage
if err = xml.Unmarshal(body, &result); err != nil {
return
}
output = result
return
}
40 changes: 40 additions & 0 deletions lib/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package lib_test

import (
. "github.com/heroku/force/lib"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Test", func() {
Describe("NewRunTestsRequest", func() {
It("should support individual test methods", func() {
request, _ := NewRunTestsRequest([]string{"MyClass.method1"}, "")
Expect(len(request.Tests)).To(Equal(1))
Expect(request.Tests[0].ClassName).To(Equal("MyClass"))
Expect(request.Tests[0].TestMethods[0]).To(Equal("method1"))
})
It("should support multiple test methods", func() {
request, _ := NewRunTestsRequest([]string{"MyClass.method1", "MyClass.method2"}, "")
Expect(len(request.Tests)).To(Equal(1))
Expect(request.Tests[0].ClassName).To(Equal("MyClass"))
Expect(request.Tests[0].TestMethods[0]).To(Equal("method1"))
Expect(request.Tests[0].TestMethods[1]).To(Equal("method2"))
})
It("should support multiple classes", func() {
request, _ := NewRunTestsRequest([]string{"MyClass", "MyOtherClass"}, "")
Expect(len(request.Tests)).To(Equal(0))
Expect(request.Classes[0]).To(Equal("MyClass"))
Expect(request.Classes[1]).To(Equal("MyOtherClass"))
})
It("should fail if only some classes specify methods", func() {
_, err := NewRunTestsRequest([]string{"MyClass", "MyOtherClass.method2"}, "")
Expect(err).To(HaveOccurred())
})
It("should fail with multiple methods from different classes", func() {
_, err := NewRunTestsRequest([]string{"MyClass.method1", "MyOtherClass.method2"}, "")
Expect(err).To(HaveOccurred())
})
})
})

0 comments on commit 133ec86

Please sign in to comment.