-
-
Notifications
You must be signed in to change notification settings - Fork 287
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package matchers | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/onsi/gomega/format" | ||
) | ||
|
||
func extractField(actual interface{}, field string) (interface{}, error) { | ||
fields := strings.SplitN(field, ".", 2) | ||
actualValue := reflect.ValueOf(actual) | ||
|
||
if actualValue.Kind() != reflect.Struct { | ||
return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1)) | ||
} | ||
|
||
var extractedValue reflect.Value | ||
|
||
if strings.HasSuffix(fields[0], "()") { | ||
extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()")) | ||
if extractedValue == (reflect.Value{}) { | ||
return nil, fmt.Errorf("HaveField could not find method named '%s' in struct of type %T.", fields[0], actual) | ||
} | ||
t := extractedValue.Type() | ||
if t.NumIn() != 0 || t.NumOut() != 1 { | ||
return nil, fmt.Errorf("HaveField found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", fields[0], actual) | ||
} | ||
extractedValue = extractedValue.Call([]reflect.Value{})[0] | ||
} else { | ||
extractedValue = actualValue.FieldByName(fields[0]) | ||
if extractedValue == (reflect.Value{}) { | ||
return nil, fmt.Errorf("HaveField could not find field named '%s' in struct:\n%s", fields[0], format.Object(actual, 1)) | ||
} | ||
} | ||
|
||
if len(fields) == 1 { | ||
return extractedValue.Interface(), nil | ||
} else { | ||
return extractField(extractedValue.Interface(), fields[1]) | ||
} | ||
} | ||
|
||
type HaveFieldMatcher struct { | ||
Field string | ||
Expected interface{} | ||
|
||
extractedField interface{} | ||
expectedMatcher omegaMatcher | ||
} | ||
|
||
func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) { | ||
matcher.extractedField, err = extractField(actual, matcher.Field) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
var isMatcher bool | ||
matcher.expectedMatcher, isMatcher = matcher.Expected.(omegaMatcher) | ||
if !isMatcher { | ||
matcher.expectedMatcher = &EqualMatcher{Expected: matcher.Expected} | ||
} | ||
|
||
return matcher.expectedMatcher.Match(matcher.extractedField) | ||
} | ||
|
||
func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) { | ||
message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field) | ||
message += matcher.expectedMatcher.FailureMessage(matcher.extractedField) | ||
|
||
return message | ||
} | ||
|
||
func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) { | ||
message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field) | ||
message += matcher.expectedMatcher.NegatedFailureMessage(matcher.extractedField) | ||
|
||
return message | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package matchers_test | ||
|
||
import ( | ||
"fmt" | ||
"time" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/ginkgo/extensions/table" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
type Book struct { | ||
Title string | ||
Author person | ||
Pages int | ||
} | ||
|
||
func (book Book) AuthorName() string { | ||
return fmt.Sprintf("%s %s", book.Author.FirstName, book.Author.LastName) | ||
} | ||
|
||
func (book Book) AbbreviatedAuthor() person { | ||
return person{ | ||
FirstName: book.Author.FirstName[0:3], | ||
LastName: book.Author.LastName[0:3], | ||
DOB: book.Author.DOB, | ||
} | ||
} | ||
|
||
func (book Book) NoReturn() { | ||
} | ||
|
||
func (book Book) TooManyReturn() (string, error) { | ||
return "", nil | ||
} | ||
|
||
func (book Book) HasArg(arg string) string { | ||
return arg | ||
} | ||
|
||
type person struct { | ||
FirstName string | ||
LastName string | ||
DOB time.Time | ||
} | ||
|
||
var _ = Describe("HaveField", func() { | ||
var book Book | ||
BeforeEach(func() { | ||
book = Book{ | ||
Title: "Les Miserables", | ||
Author: person{ | ||
FirstName: "Victor", | ||
LastName: "Hugo", | ||
DOB: time.Date(1802, 2, 26, 0, 0, 0, 0, time.UTC), | ||
}, | ||
Pages: 2783, | ||
} | ||
}) | ||
|
||
DescribeTable("traversing the struct works", | ||
func(field string, expected interface{}) { | ||
Ω(book).Should(HaveField(field, expected)) | ||
}, | ||
Entry("Top-level field with default submatcher", "Title", "Les Miserables"), | ||
Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Les Mis")), | ||
Entry("Nested field", "Author.FirstName", "Victor"), | ||
Entry("Top-level method", "AuthorName()", "Victor Hugo"), | ||
Entry("Nested method", "Author.DOB.Year()", BeNumerically("<", 1900)), | ||
Entry("Traversing past a method", "AbbreviatedAuthor().FirstName", Equal("Vic")), | ||
) | ||
|
||
DescribeTable("negation works", | ||
func(field string, expected interface{}) { | ||
Ω(book).ShouldNot(HaveField(field, expected)) | ||
}, | ||
Entry("Top-level field with default submatcher", "Title", "Les Mis"), | ||
Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Notre Dame")), | ||
Entry("Nested field", "Author.FirstName", "Hugo"), | ||
Entry("Top-level method", "AuthorName()", "Victor M. Hugo"), | ||
Entry("Nested method", "Author.DOB.Year()", BeNumerically(">", 1900)), | ||
) | ||
|
||
Describe("when field lookup fails", func() { | ||
It("errors appropriately", func() { | ||
success, err := HaveField("BookName", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName")) | ||
|
||
success, err = HaveField("BookName", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName")) | ||
|
||
success, err = HaveField("AuthorName", "Victor Hugo").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "AuthorName")) | ||
|
||
success, err = HaveField("Title()", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField could not find method named '%s' in struct of type matchers_test.Book.", "Title()")) | ||
|
||
success, err = HaveField("NoReturn()", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'NoReturn()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value.")) | ||
|
||
success, err = HaveField("TooManyReturn()", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'TooManyReturn()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value.")) | ||
|
||
success, err = HaveField("HasArg()", "Les Miserables").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'HasArg()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value.")) | ||
|
||
success, err = HaveField("Pages.Count", 2783).Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(Equal("HaveField encountered:\n <int>: 2783\nWhich is not a struct.")) | ||
|
||
success, err = HaveField("Author.Abbreviation", "Vic").Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "Abbreviation")) | ||
}) | ||
}) | ||
|
||
Describe("Failure Messages", func() { | ||
It("renders the underlying matcher failure", func() { | ||
matcher := HaveField("Title", "Les Mis") | ||
success, err := matcher.Match(book) | ||
Ω(success).Should(BeFalse()) | ||
Ω(err).ShouldNot(HaveOccurred()) | ||
|
||
msg := matcher.FailureMessage(book) | ||
Ω(msg).Should(Equal("Value for field 'Title' failed to satisfy matcher.\nExpected\n <string>: Les Miserables\nto equal\n <string>: Les Mis")) | ||
|
||
matcher = HaveField("Title", "Les Miserables") | ||
success, err = matcher.Match(book) | ||
Ω(success).Should(BeTrue()) | ||
Ω(err).ShouldNot(HaveOccurred()) | ||
|
||
msg = matcher.NegatedFailureMessage(book) | ||
Ω(msg).Should(Equal("Value for field 'Title' satisfied matcher, but should not have.\nExpected\n <string>: Les Miserables\nnot to equal\n <string>: Les Miserables")) | ||
}) | ||
}) | ||
}) |