├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── options.go ├── options_test.go ├── parser.go ├── report ├── log.go └── terminal.go ├── spec.go └── spec_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.coverprofile 4 | *.test 5 | *~ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.12.x 4 | - 1.13.x 5 | script: 6 | - test -z $(go fmt ./...) 7 | - go vet ./... 8 | - go test -v ./... 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Stephen Levine 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spec 2 | 3 | [![Build Status](https://travis-ci.org/sclevine/spec.svg?branch=master)](https://travis-ci.org/sclevine/spec) 4 | [![GoDoc](https://godoc.org/github.com/sclevine/spec?status.svg)](https://godoc.org/github.com/sclevine/spec) 5 | 6 | Spec is a simple BDD test organizer for Go. It minimally extends the standard 7 | library `testing` package by facilitating easy organization of Go 1.7+ 8 | [subtests](https://blog.golang.org/subtests). 9 | 10 | Spec differs from other BDD libraries for Go in that it: 11 | - Does not reimplement or replace any functionality of the `testing` package 12 | - Does not provide an alternative test parallelization strategy to the `testing` package 13 | - Does not provide assertions 14 | - Does not encourage the use of dot-imports 15 | - Does not reuse any closures between test runs (to avoid test pollution) 16 | - Does not use global state, excessive interface types, or reflection 17 | 18 | Spec is intended for gophers who want to write BDD tests in idiomatic Go using 19 | the standard library `testing` package. Spec aims to do "one thing right," 20 | and does not provide a wide DSL or any functionality outside of test 21 | organization. 22 | 23 | ### Features 24 | 25 | - Clean, simple syntax 26 | - Supports focusing and pending tests 27 | - Supports sequential, random, reverse, and parallel test order 28 | - Provides granular control over test order and subtest nesting 29 | - Provides a test writer to manage test output 30 | - Provides a generic, asynchronous reporting interface 31 | - Provides multiple reporter implementations 32 | 33 | ### Notes 34 | 35 | - Use `go test -v` to see individual subtests. 36 | 37 | ### Examples 38 | 39 | [Most functionality is demonstrated here.](spec_test.go#L238) 40 | 41 | Quick example: 42 | 43 | ```go 44 | func TestObject(t *testing.T) { 45 | spec.Run(t, "object", func(t *testing.T, when spec.G, it spec.S) { 46 | var someObject *myapp.Object 47 | 48 | it.Before(func() { 49 | someObject = myapp.NewObject() 50 | }) 51 | 52 | it.After(func() { 53 | someObject.Close() 54 | }) 55 | 56 | it("should have some default", func() { 57 | if someObject.Default != "value" { 58 | t.Error("bad default") 59 | } 60 | }) 61 | 62 | when("something happens", func() { 63 | it.Before(func() { 64 | someObject.Connect() 65 | }) 66 | 67 | it("should do one thing", func() { 68 | if err := someObject.DoThing(); err != nil { 69 | t.Error(err) 70 | } 71 | }) 72 | 73 | it("should do another thing", func() { 74 | if result := someObject.DoOtherThing(); result != "good result" { 75 | t.Error("bad result") 76 | } 77 | }) 78 | }, spec.Random()) 79 | 80 | when("some slow things happen", func() { 81 | it("should do one thing in parallel", func() { 82 | if result := someObject.DoSlowThing(); result != "good result" { 83 | t.Error("bad result") 84 | } 85 | }) 86 | 87 | it("should do another thing in parallel", func() { 88 | if result := someObject.DoOtherSlowThing(); result != "good result" { 89 | t.Error("bad result") 90 | } 91 | }) 92 | }, spec.Parallel()) 93 | }, spec.Report(report.Terminal{})) 94 | } 95 | ``` 96 | 97 | With less nesting: 98 | 99 | ```go 100 | func TestObject(t *testing.T) { 101 | spec.Run(t, "object", testObject, spec.Report(report.Terminal{})) 102 | } 103 | 104 | func testObject(t *testing.T, when spec.G, it spec.S) { 105 | ... 106 | } 107 | ``` 108 | 109 | For focusing/reporting across multiple files in a package: 110 | 111 | ```go 112 | var suite spec.Suite 113 | 114 | func init() { 115 | suite = spec.New("my suite", spec.Report(report.Terminal{})) 116 | suite("object", testObject) 117 | suite("other object", testOtherObject) 118 | } 119 | 120 | func TestObjects(t *testing.T) { 121 | suite.Run(t) 122 | } 123 | 124 | func testObject(t *testing.T, when spec.G, it spec.S) { 125 | ... 126 | } 127 | 128 | func testOtherObject(t *testing.T, when spec.G, it spec.S) { 129 | ... 130 | } 131 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sclevine/spec 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | ) 7 | 8 | // An Option controls the behavior of a suite, group, or spec. 9 | // Options are inherited by subgroups and subspecs. 10 | // 11 | // Example: 12 | // If the top-level Run group is specified as Random(), each subgroup will 13 | // inherit the Random() order. This means that each group will be randomized 14 | // individually, unless another ordering is specified on any of the subgroups. 15 | // If the Run group is also passed Global(), then all specs inside Run will run 16 | // in completely random order, regardless of any ordering specified on the 17 | // subgroups. 18 | type Option func(*config) 19 | 20 | // Report specifies a Reporter for a suite. 21 | // 22 | // Valid Option for: 23 | // New, Run, Focus, Pend 24 | func Report(r Reporter) Option { 25 | return func(c *config) { 26 | c.report = r 27 | } 28 | } 29 | 30 | // Seed specifies the random seed used for any randomized specs in a Run block. 31 | // The random seed is always displayed before specs are run. 32 | // If not specified, the current time is used. 33 | // 34 | // Valid Option for: 35 | // New, Run, Focus, Pend 36 | func Seed(s int64) Option { 37 | return func(c *config) { 38 | c.seed = s 39 | } 40 | } 41 | 42 | // Sequential indicates that a group of specs should be run in order. 43 | // This is the default behavior. 44 | // 45 | // Valid Option for: 46 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 47 | func Sequential() Option { 48 | return func(c *config) { 49 | c.order = orderSequential 50 | } 51 | } 52 | 53 | // Random indicates that a group of specs should be run in random order. 54 | // Randomization is per group, such that all groupings are maintained. 55 | // 56 | // Valid Option for: 57 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 58 | func Random() Option { 59 | return func(c *config) { 60 | c.order = orderRandom 61 | } 62 | } 63 | 64 | // Reverse indicates that a group of specs should be run in reverse order. 65 | // 66 | // Valid Option for: 67 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 68 | func Reverse() Option { 69 | return func(c *config) { 70 | c.order = orderReverse 71 | } 72 | } 73 | 74 | // Parallel indicates that a spec or group of specs should be run in parallel. 75 | // This Option is equivalent to t.Parallel(). 76 | // 77 | // Valid Option for: 78 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend, S 79 | func Parallel() Option { 80 | return func(c *config) { 81 | c.order = orderParallel 82 | } 83 | } 84 | 85 | // Local indicates that the test order applies to each subgroup individually. 86 | // For example, a group with Random() and Local() will run all subgroups and 87 | // specs in random order, and each subgroup will be randomized, but specs in 88 | // different subgroups will not be interleaved. 89 | // This is the default behavior. 90 | // 91 | // Valid Option for: 92 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 93 | func Local() Option { 94 | return func(c *config) { 95 | c.scope = scopeLocal 96 | } 97 | } 98 | 99 | // Global indicates that test order applies globally to all descendant specs. 100 | // For example, a group with Random() and Global() will run all descendant 101 | // specs in random order, regardless of subgroup. Specs in different subgroups 102 | // may be interleaved. 103 | // 104 | // Valid Option for: 105 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 106 | func Global() Option { 107 | return func(c *config) { 108 | c.scope = scopeGlobal 109 | } 110 | } 111 | 112 | // Flat indicates that a parent subtest should not be created for the group. 113 | // This is the default behavior. 114 | // 115 | // Valid Option for: 116 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 117 | func Flat() Option { 118 | return func(c *config) { 119 | c.nest = nestOff 120 | } 121 | } 122 | 123 | // Nested indicates that a parent subtest should be created for the group. 124 | // This allows for more control over parallelism. 125 | // 126 | // Valid Option for: 127 | // New, Run, Focus, Pend, Suite, Suite.Focus, Suite.Pend, G, G.Focus, G.Pend 128 | func Nested() Option { 129 | return func(c *config) { 130 | c.nest = nestOn 131 | } 132 | } 133 | 134 | type order int 135 | 136 | const ( 137 | orderInherit order = iota 138 | orderSequential 139 | orderParallel 140 | orderRandom 141 | orderReverse 142 | ) 143 | 144 | func (o order) or(last order) order { 145 | return order(defaultZero(int(o), int(last))) 146 | } 147 | 148 | type scope int 149 | 150 | const ( 151 | scopeInherit scope = iota 152 | scopeLocal 153 | scopeGlobal 154 | ) 155 | 156 | func (s scope) or(last scope) scope { 157 | return scope(defaultZero(int(s), int(last))) 158 | } 159 | 160 | type nest int 161 | 162 | const ( 163 | nestInherit nest = iota 164 | nestOff 165 | nestOn 166 | ) 167 | 168 | func (n nest) or(last nest) nest { 169 | return nest(defaultZero(int(n), int(last))) 170 | } 171 | 172 | func defaultZero(next, last int) int { 173 | if next == 0 { 174 | return last 175 | } 176 | return next 177 | } 178 | 179 | func defaultZero64(next, last int64) int64 { 180 | if next == 0 { 181 | return last 182 | } 183 | return next 184 | } 185 | 186 | type config struct { 187 | seed int64 188 | order order 189 | scope scope 190 | nest nest 191 | pend bool 192 | focus bool 193 | before bool 194 | after bool 195 | t *testing.T 196 | out func(io.Writer) 197 | report Reporter 198 | } 199 | 200 | type options []Option 201 | 202 | func (o options) apply() *config { 203 | cfg := &config{} 204 | for _, opt := range o { 205 | opt(cfg) 206 | } 207 | return cfg 208 | } 209 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package spec_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/sclevine/spec" 11 | ) 12 | 13 | func optionTestSpec(t *testing.T, it spec.S, s recorder, name string) { 14 | if name != "" { 15 | name += "." 16 | } 17 | it.Before(s(t, name+"Before.1")) 18 | it.Before(s(t, name+"Before.2")) 19 | it.Before(s(t, name+"Before.3")) 20 | it.After(s(t, name+"After.1")) 21 | it.After(s(t, name+"After.2")) 22 | it.After(s(t, name+"After.3")) 23 | it(name+"S", s(t, name+"S")) 24 | } 25 | 26 | func optionTestSpecs(t *testing.T, it spec.S, s recorder, name string) { 27 | if name != "" { 28 | name += "." 29 | } 30 | it(name+"S.1", s(t, name+"S.1")) 31 | it(name+"S.2", s(t, name+"S.2")) 32 | it(name+"S.3", s(t, name+"S.3")) 33 | } 34 | 35 | func optionTestCases(t *testing.T, when spec.G, it spec.S, s recorder) { 36 | optionTestSpecs(t, it, s, "") 37 | when("G", func() { 38 | optionTestSpec(t, it, s, "G") 39 | }) 40 | when("G.Sequential", func() { 41 | optionTestSpecs(t, it, s, "G.Sequential") 42 | }, spec.Sequential()) 43 | when("G.Reverse", func() { 44 | optionTestSpecs(t, it, s, "G.Reverse") 45 | }, spec.Reverse()) 46 | when("G.Random.Local", func() { 47 | optionTestSpecs(t, it, s, "G.Random.Local") 48 | }, spec.Random(), spec.Local()) 49 | when("G.Random.Global", func() { 50 | optionTestSpecs(t, it, s, "G.Random.Global") 51 | }, spec.Random(), spec.Global()) 52 | } 53 | 54 | func optionDefaultOrder(t *testing.T, name string, seed int64) []string { 55 | s, calls := record(t) 56 | 57 | spec.Run(t, name, func(t *testing.T, when spec.G, it spec.S) { 58 | optionTestCases(t, when, it, s) 59 | }, spec.Seed(seed)) 60 | 61 | results := calls() 62 | for i := range results { 63 | results[i] = regexp.MustCompile(`#[0-9]+`).ReplaceAllLiteralString(results[i], "") 64 | } 65 | 66 | return results 67 | } 68 | 69 | type testReporter struct { 70 | StartT, SpecsT *testing.T 71 | StartPlan spec.Plan 72 | SpecOrder []spec.Spec 73 | } 74 | 75 | func (tr *testReporter) Start(t *testing.T, plan spec.Plan) { 76 | tr.StartT = t 77 | tr.StartPlan = plan 78 | } 79 | 80 | func (tr *testReporter) Specs(t *testing.T, specs <-chan spec.Spec) { 81 | tr.SpecsT = t 82 | for s := range specs { 83 | tr.SpecOrder = append(tr.SpecOrder, s) 84 | } 85 | } 86 | 87 | func TestReport(t *testing.T) { 88 | s, calls := record(t) 89 | reporter := &testReporter{} 90 | 91 | suite := spec.New("Suite", spec.Report(reporter), spec.Seed(2)) 92 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 93 | when("Top.G.Out", func() { 94 | it("Top.G.Out.1", func() { 95 | fmt.Fprint(it.Out(), "Top.G.Out.1") 96 | }) 97 | it("Top.G.Out.2", func() { 98 | fmt.Fprint(it.Out(), "Top.G.Out.2") 99 | }) 100 | }, spec.Reverse()) 101 | optionTestCases(t, when, it, s) 102 | }) 103 | suite.Run(t) 104 | 105 | if !reflect.DeepEqual(calls(), optionDefaultOrder(t, "Suite/Top", 2)) { 106 | t.Fatal("Incorrect order:", calls()) 107 | } 108 | if reporter.StartT != t { 109 | t.Fatal("Incorrect value for t on start.") 110 | } 111 | if reporter.SpecsT != t { 112 | t.Fatal("Incorrect value for t on spec run.") 113 | } 114 | if reporter.StartPlan != (spec.Plan{ 115 | Text: "Suite", 116 | Total: 18, 117 | Pending: 0, 118 | Focused: 0, 119 | Seed: 2, 120 | HasRandom: true, 121 | HasFocus: false, 122 | }) { 123 | t.Fatal("Incorrect plan:", reporter.StartPlan) 124 | } 125 | 126 | out2, err := ioutil.ReadAll(reporter.SpecOrder[0].Out) 127 | if string(out2) != "Top.G.Out.2" || err != nil { 128 | t.Fatal("Incorrect output for Top.G.Out.2 buffer.") 129 | } 130 | out1, err := ioutil.ReadAll(reporter.SpecOrder[1].Out) 131 | if string(out1) != "Top.G.Out.1" || err != nil { 132 | t.Fatal("Incorrect output for Top.G.Out.1 buffer.") 133 | } 134 | empty, err := ioutil.ReadAll(reporter.SpecOrder[2].Out) 135 | if string(empty) != "" || err != nil { 136 | t.Fatal("Incorrect output for empty buffer.") 137 | } 138 | for i := range reporter.SpecOrder { 139 | reporter.SpecOrder[i].Out = nil 140 | } 141 | 142 | if !reflect.DeepEqual(reporter.SpecOrder, []spec.Spec{ 143 | {Text: []string{"Top", "Top.G.Out", "Top.G.Out.2"}}, {Text: []string{"Top", "Top.G.Out", "Top.G.Out.1"}}, 144 | {Text: []string{"Top", "S.1"}}, {Text: []string{"Top", "S.2"}}, {Text: []string{"Top", "S.3"}}, 145 | {Text: []string{"Top", "G", "G.S"}}, 146 | {Text: []string{"Top", "G.Sequential", "G.Sequential.S.1"}}, 147 | {Text: []string{"Top", "G.Sequential", "G.Sequential.S.2"}}, 148 | {Text: []string{"Top", "G.Sequential", "G.Sequential.S.3"}}, 149 | {Text: []string{"Top", "G.Reverse", "G.Reverse.S.3"}}, 150 | {Text: []string{"Top", "G.Reverse", "G.Reverse.S.2"}}, 151 | {Text: []string{"Top", "G.Reverse", "G.Reverse.S.1"}}, 152 | {Text: []string{"Top", "G.Random.Local", "G.Random.Local.S.3"}}, 153 | {Text: []string{"Top", "G.Random.Local", "G.Random.Local.S.1"}}, 154 | {Text: []string{"Top", "G.Random.Local", "G.Random.Local.S.2"}}, 155 | {Text: []string{"Top", "G.Random.Global", "G.Random.Global.S.3"}}, 156 | {Text: []string{"Top", "G.Random.Global", "G.Random.Global.S.1"}}, 157 | {Text: []string{"Top", "G.Random.Global", "G.Random.Global.S.2"}}, 158 | }) { 159 | t.Fatal("Incorrect reported order:", reporter.SpecOrder) 160 | } 161 | } 162 | 163 | func TestDefault(t *testing.T) { 164 | s, calls := record(t) 165 | 166 | suite := spec.New("Suite", spec.Seed(2)) 167 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 168 | optionTestCases(t, when, it, s) 169 | }) 170 | suite.Run(t) 171 | 172 | if !reflect.DeepEqual(calls(), []string{ 173 | "Suite/Top/S.1->S.1", "Suite/Top/S.2->S.2", "Suite/Top/S.3->S.3", 174 | 175 | "Suite/Top/G/G.S->G.Before.1", "Suite/Top/G/G.S->G.Before.2", "Suite/Top/G/G.S->G.Before.3", 176 | "Suite/Top/G/G.S->G.S", 177 | "Suite/Top/G/G.S->G.After.1", "Suite/Top/G/G.S->G.After.2", "Suite/Top/G/G.S->G.After.3", 178 | 179 | "Suite/Top/G.Sequential/G.Sequential.S.1->G.Sequential.S.1", 180 | "Suite/Top/G.Sequential/G.Sequential.S.2->G.Sequential.S.2", 181 | "Suite/Top/G.Sequential/G.Sequential.S.3->G.Sequential.S.3", 182 | 183 | "Suite/Top/G.Reverse/G.Reverse.S.3->G.Reverse.S.3", 184 | "Suite/Top/G.Reverse/G.Reverse.S.2->G.Reverse.S.2", 185 | "Suite/Top/G.Reverse/G.Reverse.S.1->G.Reverse.S.1", 186 | 187 | "Suite/Top/G.Random.Local/G.Random.Local.S.3->G.Random.Local.S.3", 188 | "Suite/Top/G.Random.Local/G.Random.Local.S.1->G.Random.Local.S.1", 189 | "Suite/Top/G.Random.Local/G.Random.Local.S.2->G.Random.Local.S.2", 190 | 191 | "Suite/Top/G.Random.Global/G.Random.Global.S.3->G.Random.Global.S.3", 192 | "Suite/Top/G.Random.Global/G.Random.Global.S.1->G.Random.Global.S.1", 193 | "Suite/Top/G.Random.Global/G.Random.Global.S.2->G.Random.Global.S.2", 194 | }) { 195 | t.Fatal("Incorrect order:", calls()) 196 | } 197 | } 198 | 199 | func TestSequential(t *testing.T) { 200 | s, calls := record(t) 201 | 202 | suite := spec.New("Suite", spec.Sequential(), spec.Seed(2)) 203 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 204 | optionTestCases(t, when, it, s) 205 | }) 206 | suite.Run(t) 207 | 208 | if !reflect.DeepEqual(calls(), optionDefaultOrder(t, "Suite/Top", 2)) { 209 | t.Fatal("Incorrect order:", calls()) 210 | } 211 | } 212 | 213 | func TestRandom(t *testing.T) { 214 | s, calls := record(t) 215 | 216 | suite := spec.New("Suite", spec.Random(), spec.Seed(2)) 217 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 218 | optionTestCases(t, when, it, s) 219 | }) 220 | suite.Run(t) 221 | 222 | if !reflect.DeepEqual(calls(), []string{ 223 | "Suite/Top/G.Random.Local/G.Random.Local.S.3->G.Random.Local.S.3", 224 | "Suite/Top/G.Random.Local/G.Random.Local.S.1->G.Random.Local.S.1", 225 | "Suite/Top/G.Random.Local/G.Random.Local.S.2->G.Random.Local.S.2", 226 | 227 | "Suite/Top/G/G.S->G.Before.1", "Suite/Top/G/G.S->G.Before.2", "Suite/Top/G/G.S->G.Before.3", 228 | "Suite/Top/G/G.S->G.S", 229 | "Suite/Top/G/G.S->G.After.1", "Suite/Top/G/G.S->G.After.2", "Suite/Top/G/G.S->G.After.3", 230 | 231 | "Suite/Top/G.Random.Global/G.Random.Global.S.3->G.Random.Global.S.3", 232 | "Suite/Top/G.Random.Global/G.Random.Global.S.1->G.Random.Global.S.1", 233 | "Suite/Top/G.Random.Global/G.Random.Global.S.2->G.Random.Global.S.2", 234 | 235 | "Suite/Top/G.Sequential/G.Sequential.S.1->G.Sequential.S.1", 236 | "Suite/Top/G.Sequential/G.Sequential.S.2->G.Sequential.S.2", 237 | "Suite/Top/G.Sequential/G.Sequential.S.3->G.Sequential.S.3", 238 | 239 | "Suite/Top/G.Reverse/G.Reverse.S.3->G.Reverse.S.3", 240 | "Suite/Top/G.Reverse/G.Reverse.S.2->G.Reverse.S.2", 241 | "Suite/Top/G.Reverse/G.Reverse.S.1->G.Reverse.S.1", 242 | 243 | "Suite/Top/S.1->S.1", "Suite/Top/S.2->S.2", "Suite/Top/S.3->S.3", 244 | }) { 245 | t.Fatal("Incorrect order:", calls()) 246 | } 247 | } 248 | 249 | func TestReverse(t *testing.T) { 250 | s, calls := record(t) 251 | 252 | suite := spec.New("Suite", spec.Reverse(), spec.Seed(2)) 253 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 254 | optionTestCases(t, when, it, s) 255 | }) 256 | suite.Run(t) 257 | 258 | if !reflect.DeepEqual(calls(), []string{ 259 | "Suite/Top/G.Random.Global/G.Random.Global.S.3->G.Random.Global.S.3", 260 | "Suite/Top/G.Random.Global/G.Random.Global.S.1->G.Random.Global.S.1", 261 | "Suite/Top/G.Random.Global/G.Random.Global.S.2->G.Random.Global.S.2", 262 | 263 | "Suite/Top/G.Random.Local/G.Random.Local.S.3->G.Random.Local.S.3", 264 | "Suite/Top/G.Random.Local/G.Random.Local.S.1->G.Random.Local.S.1", 265 | "Suite/Top/G.Random.Local/G.Random.Local.S.2->G.Random.Local.S.2", 266 | 267 | "Suite/Top/G.Reverse/G.Reverse.S.3->G.Reverse.S.3", 268 | "Suite/Top/G.Reverse/G.Reverse.S.2->G.Reverse.S.2", 269 | "Suite/Top/G.Reverse/G.Reverse.S.1->G.Reverse.S.1", 270 | 271 | "Suite/Top/G.Sequential/G.Sequential.S.1->G.Sequential.S.1", 272 | "Suite/Top/G.Sequential/G.Sequential.S.2->G.Sequential.S.2", 273 | "Suite/Top/G.Sequential/G.Sequential.S.3->G.Sequential.S.3", 274 | 275 | "Suite/Top/G/G.S->G.Before.1", "Suite/Top/G/G.S->G.Before.2", "Suite/Top/G/G.S->G.Before.3", 276 | "Suite/Top/G/G.S->G.S", 277 | "Suite/Top/G/G.S->G.After.1", "Suite/Top/G/G.S->G.After.2", "Suite/Top/G/G.S->G.After.3", 278 | 279 | "Suite/Top/S.3->S.3", "Suite/Top/S.2->S.2", "Suite/Top/S.1->S.1", 280 | }) { 281 | t.Fatal("Incorrect order:", calls()) 282 | } 283 | } 284 | 285 | func TestLocal(t *testing.T) { 286 | s, calls := record(t) 287 | 288 | suite := spec.New("Suite", spec.Local(), spec.Seed(2)) 289 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 290 | optionTestCases(t, when, it, s) 291 | }) 292 | suite.Run(t) 293 | 294 | if !reflect.DeepEqual(calls(), optionDefaultOrder(t, "Suite/Top", 2)) { 295 | t.Fatal("Incorrect order:", calls()) 296 | } 297 | } 298 | 299 | func TestGlobal(t *testing.T) { 300 | s, calls := record(t) 301 | 302 | suite := spec.New("Suite", spec.Random(), spec.Seed(2), spec.Global()) 303 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 304 | optionTestCases(t, when, it, s) 305 | }) 306 | suite.Run(t) 307 | 308 | if !reflect.DeepEqual(calls(), []string{ 309 | "Suite/Top/G.Reverse/G.Reverse.S.3->G.Reverse.S.3", 310 | 311 | "Suite/Top/G.Random.Global/G.Random.Global.S.2->G.Random.Global.S.2", 312 | 313 | "Suite/Top/S.1->S.1", 314 | 315 | "Suite/Top/G.Random.Local/G.Random.Local.S.3->G.Random.Local.S.3", 316 | "Suite/Top/G.Random.Local/G.Random.Local.S.1->G.Random.Local.S.1", 317 | "Suite/Top/G.Random.Local/G.Random.Local.S.2->G.Random.Local.S.2", 318 | 319 | "Suite/Top/G.Sequential/G.Sequential.S.3->G.Sequential.S.3", 320 | 321 | "Suite/Top/S.2->S.2", 322 | 323 | "Suite/Top/G.Reverse/G.Reverse.S.1->G.Reverse.S.1", 324 | 325 | "Suite/Top/S.3->S.3", 326 | 327 | "Suite/Top/G.Reverse/G.Reverse.S.2->G.Reverse.S.2", 328 | 329 | "Suite/Top/G.Sequential/G.Sequential.S.1->G.Sequential.S.1", 330 | 331 | "Suite/Top/G.Random.Global/G.Random.Global.S.3->G.Random.Global.S.3", 332 | 333 | "Suite/Top/G/G.S->G.Before.1", "Suite/Top/G/G.S->G.Before.2", "Suite/Top/G/G.S->G.Before.3", 334 | "Suite/Top/G/G.S->G.S", 335 | "Suite/Top/G/G.S->G.After.1", "Suite/Top/G/G.S->G.After.2", "Suite/Top/G/G.S->G.After.3", 336 | 337 | "Suite/Top/G.Random.Global/G.Random.Global.S.1->G.Random.Global.S.1", 338 | 339 | "Suite/Top/G.Sequential/G.Sequential.S.2->G.Sequential.S.2", 340 | }) { 341 | t.Fatal("Incorrect order:", calls()) 342 | } 343 | } 344 | 345 | func TestFlat(t *testing.T) { 346 | s, calls := record(t) 347 | 348 | suite := spec.New("Suite", spec.Flat(), spec.Seed(2)) 349 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 350 | optionTestCases(t, when, it, s) 351 | }) 352 | suite.Run(t) 353 | 354 | if !reflect.DeepEqual(calls(), optionDefaultOrder(t, "Suite/Top", 2)) { 355 | t.Fatal("Incorrect order:", calls()) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | type node struct { 10 | text []string 11 | loc []int 12 | seed int64 13 | order order 14 | scope scope 15 | nest nest 16 | pend bool 17 | focus bool 18 | nodes tree 19 | } 20 | 21 | func (n *node) parse(f func(*testing.T, G, S)) Plan { 22 | // TODO: validate Options 23 | plan := Plan{ 24 | Text: strings.Join(n.text, "/"), 25 | Seed: n.seed, 26 | } 27 | f(nil, func(text string, f func(), opts ...Option) { 28 | cfg := options(opts).apply() 29 | n.add(text, cfg, tree{}) 30 | parent := n 31 | n = n.last() 32 | plan.update(n) 33 | defer func() { 34 | n.level() 35 | n.sort() 36 | n = parent 37 | }() 38 | f() 39 | }, func(text string, _ func(), opts ...Option) { 40 | cfg := options(opts).apply() 41 | if cfg.before || cfg.after || cfg.out != nil { 42 | return 43 | } 44 | n.add(text, cfg, nil) 45 | plan.update(n.last()) 46 | }) 47 | n.level() 48 | n.sort() 49 | return plan 50 | } 51 | 52 | func (p *Plan) update(n *node) { 53 | if n.focus && !n.pend { 54 | p.HasFocus = true 55 | } 56 | if n.order == orderRandom { 57 | p.HasRandom = true 58 | } 59 | if n.nodes == nil { 60 | p.Total++ 61 | if n.focus && !n.pend { 62 | p.Focused++ 63 | } else if n.pend { 64 | p.Pending++ 65 | } 66 | } 67 | } 68 | 69 | func (n *node) add(text string, cfg *config, nodes tree) { 70 | name := n.text 71 | if n.nested() { 72 | name = nil 73 | } 74 | n.nodes = append(n.nodes, node{ 75 | text: append(append([]string(nil), name...), text), 76 | loc: append(append([]int(nil), n.loc...), len(n.nodes)), 77 | seed: n.seed, 78 | order: cfg.order.or(n.order), 79 | scope: cfg.scope.or(n.scope), 80 | nest: cfg.nest.or(n.nest), 81 | pend: cfg.pend || n.pend, 82 | focus: cfg.focus || n.focus, 83 | nodes: nodes, 84 | }) 85 | } 86 | 87 | func (n *node) sort() { 88 | nodes := n.nodes 89 | switch n.order { 90 | case orderRandom: 91 | r := rand.New(rand.NewSource(n.seed)) 92 | for i := len(nodes) - 1; i > 0; i-- { 93 | j := r.Intn(i + 1) 94 | nodes[i], nodes[j] = nodes[j], nodes[i] 95 | } 96 | case orderReverse: 97 | last := len(nodes) - 1 98 | for i := 0; i < len(nodes)/2; i++ { 99 | nodes[i], nodes[last-i] = nodes[last-i], nodes[i] 100 | } 101 | } 102 | } 103 | 104 | func (n *node) level() { 105 | nodes := n.nodes 106 | switch n.scope { 107 | case scopeGlobal: 108 | var flat tree 109 | for _, child := range nodes { 110 | if child.nodes == nil || child.scope == scopeLocal { 111 | flat = append(flat, child) 112 | } else { 113 | flat = append(flat, child.nodes...) 114 | } 115 | } 116 | n.nodes = flat 117 | } 118 | } 119 | 120 | func (n *node) last() *node { 121 | return &n.nodes[len(n.nodes)-1] 122 | } 123 | 124 | func (n *node) nested() bool { 125 | return n.nest == nestOn || len(n.loc) == 0 126 | } 127 | 128 | func (n node) run(t *testing.T, f func(*testing.T, node)) bool { 129 | t.Helper() 130 | name := strings.Join(n.text, "/") 131 | switch { 132 | case n.nodes == nil: 133 | return t.Run(name, func(t *testing.T) { f(t, n) }) 134 | case n.nested(): 135 | return t.Run(name, func(t *testing.T) { n.nodes.run(t, f) }) 136 | default: 137 | return n.nodes.run(t, f) 138 | } 139 | } 140 | 141 | type tree []node 142 | 143 | func (ns tree) run(t *testing.T, f func(*testing.T, node)) bool { 144 | t.Helper() 145 | ok := true 146 | for _, n := range ns { 147 | ok = n.run(t, f) && ok 148 | } 149 | return ok 150 | } 151 | -------------------------------------------------------------------------------- /report/log.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/sclevine/spec" 8 | ) 9 | 10 | // Log reports specs via the testing log methods and only affects verbose runs. 11 | type Log struct{} 12 | 13 | func (Log) Start(t *testing.T, plan spec.Plan) { 14 | t.Helper() 15 | t.Log("Suite:", plan.Text) 16 | t.Logf("Total: %d | Focused: %d | Pending: %d", plan.Total, plan.Focused, plan.Pending) 17 | if plan.HasRandom { 18 | t.Log("Random seed:", plan.Seed) 19 | } 20 | if plan.HasFocus { 21 | t.Log("Focus is active.") 22 | } 23 | } 24 | 25 | func (Log) Specs(t *testing.T, specs <-chan spec.Spec) { 26 | t.Helper() 27 | var passed, failed, skipped int 28 | for s := range specs { 29 | switch { 30 | case s.Failed: 31 | failed++ 32 | if testing.Verbose() { 33 | if out, err := ioutil.ReadAll(s.Out); err == nil { 34 | t.Logf("%s", out) 35 | } 36 | } 37 | case s.Skipped: 38 | skipped++ 39 | default: 40 | passed++ 41 | } 42 | } 43 | t.Logf("Passed: %d | Failed: %d | Skipped: %d", passed, failed, skipped) 44 | } 45 | -------------------------------------------------------------------------------- /report/terminal.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/sclevine/spec" 9 | ) 10 | 11 | // Terminal reports specs via stdout. 12 | type Terminal struct{} 13 | 14 | func (Terminal) Start(_ *testing.T, plan spec.Plan) { 15 | fmt.Println("Suite:", plan.Text) 16 | fmt.Printf("Total: %d | Focused: %d | Pending: %d\n", plan.Total, plan.Focused, plan.Pending) 17 | if plan.HasRandom { 18 | fmt.Println("Random seed:", plan.Seed) 19 | } 20 | if plan.HasFocus { 21 | fmt.Println("Focus is active.") 22 | } 23 | } 24 | 25 | func (Terminal) Specs(_ *testing.T, specs <-chan spec.Spec) { 26 | var passed, failed, skipped int 27 | for s := range specs { 28 | switch { 29 | case s.Failed: 30 | failed++ 31 | if !testing.Verbose() { 32 | fmt.Print("x") 33 | } else { 34 | if out, err := ioutil.ReadAll(s.Out); err == nil { 35 | fmt.Printf("%s\n", out) 36 | } 37 | } 38 | case s.Skipped: 39 | skipped++ 40 | if !testing.Verbose() { 41 | fmt.Print("s") 42 | } 43 | default: 44 | passed++ 45 | if !testing.Verbose() { 46 | fmt.Print(".") 47 | } 48 | } 49 | } 50 | fmt.Printf("\nPassed: %d | Failed: %d | Skipped: %d\n\n", passed, failed, skipped) 51 | } 52 | -------------------------------------------------------------------------------- /spec.go: -------------------------------------------------------------------------------- 1 | package spec 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // G defines a group of specs. 11 | // Unlike other testing libraries, it is re-evaluated for each subspec. 12 | // 13 | // Valid Options: 14 | // Sequential, Random, Reverse, Parallel 15 | // Local, Global, Flat, Nested 16 | type G func(text string, f func(), opts ...Option) 17 | 18 | // Pend skips all specs in the provided group. 19 | // 20 | // All Options are ignored. 21 | func (g G) Pend(text string, f func(), _ ...Option) { 22 | g(text, f, func(c *config) { c.pend = true }) 23 | } 24 | 25 | // Focus focuses the provided group. 26 | // This skips all specs in the suite except the group and other focused specs. 27 | // 28 | // Valid Options: 29 | // Sequential, Random, Reverse, Parallel 30 | // Local, Global, Flat, Nested 31 | func (g G) Focus(text string, f func(), opts ...Option) { 32 | g(text, f, append(opts, func(c *config) { c.focus = true })...) 33 | } 34 | 35 | // S defines a spec. 36 | // 37 | // Valid Options: Parallel 38 | type S func(text string, f func(), opts ...Option) 39 | 40 | // Before runs a function before each spec in the group. 41 | func (s S) Before(f func()) { 42 | s("", f, func(c *config) { c.before = true }) 43 | } 44 | 45 | // After runs a function after each spec in the group. 46 | func (s S) After(f func()) { 47 | s("", f, func(c *config) { c.after = true }) 48 | } 49 | 50 | // Pend skips the provided spec. 51 | // 52 | // All Options are ignored. 53 | func (s S) Pend(text string, f func(), _ ...Option) { 54 | s(text, f, func(c *config) { c.pend = true }) 55 | } 56 | 57 | // Focus focuses the provided spec. 58 | // This skips all specs in the suite except the spec and other focused specs. 59 | // 60 | // Valid Options: Parallel 61 | func (s S) Focus(text string, f func(), opts ...Option) { 62 | s(text, f, append(opts, func(c *config) { c.focus = true })...) 63 | } 64 | 65 | // Out provides an dedicated writer for the test to store output. 66 | // Reporters usually display the contents on test failure. 67 | // 68 | // Valid context: inside S blocks only, nil elsewhere 69 | func (s S) Out() io.Writer { 70 | var out io.Writer 71 | s("", nil, func(c *config) { 72 | c.out = func(w io.Writer) { 73 | out = w 74 | } 75 | }) 76 | return out 77 | } 78 | 79 | // Suite defines a top-level group of specs within a suite. 80 | // Suite behaves like a top-level version of G. 81 | // Unlike other testing libraries, it is re-evaluated for each subspec. 82 | // 83 | // Valid Options: 84 | // Sequential, Random, Reverse, Parallel 85 | // Local, Global, Flat, Nested 86 | type Suite func(text string, f func(*testing.T, G, S), opts ...Option) bool 87 | 88 | // Before runs a function before each spec in the suite. 89 | func (s Suite) Before(f func(*testing.T)) bool { 90 | return s("", func(t *testing.T, _ G, _ S) { 91 | t.Helper() 92 | f(t) 93 | }, func(c *config) { c.before = true }) 94 | } 95 | 96 | // After runs a function after each spec in the suite. 97 | func (s Suite) After(f func(*testing.T)) bool { 98 | return s("", func(t *testing.T, _ G, _ S) { 99 | t.Helper() 100 | f(t) 101 | }, func(c *config) { c.after = true }) 102 | } 103 | 104 | // Pend skips the provided top-level group of specs. 105 | // 106 | // All Options are ignored. 107 | func (s Suite) Pend(text string, f func(*testing.T, G, S), _ ...Option) bool { 108 | return s(text, f, func(c *config) { c.pend = true }) 109 | } 110 | 111 | // Focus focuses the provided top-level group. 112 | // This skips all specs in the suite except the group and other focused specs. 113 | // 114 | // Valid Options: 115 | // Sequential, Random, Reverse, Parallel 116 | // Local, Global, Flat, Nested 117 | func (s Suite) Focus(text string, f func(*testing.T, G, S), opts ...Option) bool { 118 | return s(text, f, append(opts, func(c *config) { c.focus = true })...) 119 | } 120 | 121 | // Run executes the specs defined in each top-level group of the suite. 122 | func (s Suite) Run(t *testing.T) bool { 123 | t.Helper() 124 | return s("", nil, func(c *config) { c.t = t }) 125 | } 126 | 127 | // New creates an empty suite and returns an Suite function. 128 | // The Suite function may be called to add top-level groups to the suite. 129 | // The suite may be executed with Suite.Run. 130 | // 131 | // Valid Options: 132 | // Sequential, Random, Reverse, Parallel 133 | // Local, Global, Flat, Nested 134 | // Seed, Report 135 | func New(text string, opts ...Option) Suite { 136 | var fs []func(*testing.T, G, S) 137 | return func(newText string, f func(*testing.T, G, S), newOpts ...Option) bool { 138 | cfg := options(newOpts).apply() 139 | if cfg.t == nil { 140 | fs = append(fs, func(t *testing.T, g G, s S) { 141 | var do func(string, func(), ...Option) = g 142 | if cfg.before || cfg.after { 143 | do = s 144 | } 145 | do(newText, func() { f(t, g, s) }, newOpts...) 146 | }) 147 | return true 148 | } 149 | cfg.t.Helper() 150 | return Run(cfg.t, text, func(t *testing.T, g G, s S) { 151 | for _, f := range fs { 152 | f(t, g, s) 153 | } 154 | }, opts...) 155 | } 156 | } 157 | 158 | // Run immediately executes the provided specs as a suite. 159 | // Unlike other testing libraries, it is re-evaluated for each spec. 160 | // 161 | // Valid Options: 162 | // Sequential, Random, Reverse, Parallel 163 | // Local, Global, Flat, Nested 164 | // Seed, Report 165 | func Run(t *testing.T, text string, f func(*testing.T, G, S), opts ...Option) bool { 166 | t.Helper() 167 | cfg := options(opts).apply() 168 | n := &node{ 169 | text: []string{text}, 170 | seed: defaultZero64(cfg.seed, time.Now().Unix()), 171 | order: cfg.order.or(orderSequential), 172 | scope: cfg.scope.or(scopeLocal), 173 | nest: cfg.nest.or(nestOff), 174 | pend: cfg.pend, 175 | focus: cfg.focus, 176 | } 177 | report := cfg.report 178 | plan := n.parse(f) 179 | 180 | var specs chan Spec 181 | if report != nil { 182 | report.Start(t, plan) 183 | specs = make(chan Spec, plan.Total) 184 | done := make(chan struct{}) 185 | defer func() { 186 | close(specs) 187 | <-done 188 | }() 189 | go func() { 190 | report.Specs(t, specs) 191 | close(done) 192 | }() 193 | } 194 | 195 | return n.run(t, func(t *testing.T, n node) { 196 | t.Helper() 197 | buffer := &bytes.Buffer{} 198 | defer func() { 199 | if specs == nil { 200 | return 201 | } 202 | specs <- Spec{ 203 | Text: n.text, 204 | Failed: t.Failed(), 205 | Skipped: t.Skipped(), 206 | Focused: n.focus, 207 | Parallel: n.order == orderParallel, 208 | Out: buffer, 209 | } 210 | }() 211 | switch { 212 | case n.pend, plan.HasFocus && !n.focus: 213 | t.SkipNow() 214 | case n.order == orderParallel: 215 | t.Parallel() 216 | } 217 | 218 | var spec, group func() 219 | hooks := newHooks() 220 | group = func() {} 221 | 222 | f(t, func(_ string, f func(), _ ...Option) { 223 | switch { 224 | case len(n.loc) == 1, n.loc[0] > 0: 225 | n.loc[0]-- 226 | case n.loc[0] == 0: 227 | group = func() { 228 | n.loc = n.loc[1:] 229 | hooks.next() 230 | group = func() {} 231 | f() 232 | group() 233 | } 234 | n.loc[0]-- 235 | } 236 | }, func(_ string, f func(), opts ...Option) { 237 | cfg := options(opts).apply() 238 | switch { 239 | case cfg.out != nil: 240 | cfg.out(buffer) 241 | case cfg.before: 242 | hooks.before(f) 243 | case cfg.after: 244 | hooks.after(f) 245 | case spec != nil: 246 | case len(n.loc) > 1, n.loc[0] > 0: 247 | n.loc[0]-- 248 | default: 249 | spec = f 250 | } 251 | }) 252 | group() 253 | 254 | if spec == nil { 255 | t.Fatal("Failed to locate spec.") 256 | } 257 | hooks.run(t, spec) 258 | }) 259 | } 260 | 261 | type specHooks struct { 262 | first, last *specHook 263 | } 264 | 265 | type specHook struct { 266 | before, after []func() 267 | next *specHook 268 | } 269 | 270 | func newHooks() specHooks { 271 | h := &specHook{} 272 | return specHooks{first: h, last: h} 273 | } 274 | 275 | func (s specHooks) run(t *testing.T, spec func()) { 276 | t.Helper() 277 | for h := s.first; h != nil; h = h.next { 278 | defer run(t, h.after...) 279 | run(t, h.before...) 280 | } 281 | run(t, spec) 282 | } 283 | 284 | func (s specHooks) before(f func()) { 285 | s.last.before = append(s.last.before, f) 286 | } 287 | 288 | func (s specHooks) after(f func()) { 289 | s.last.after = append(s.last.after, f) 290 | } 291 | 292 | func (s *specHooks) next() { 293 | s.last.next = &specHook{} 294 | s.last = s.last.next 295 | } 296 | 297 | func run(t *testing.T, fs ...func()) { 298 | t.Helper() 299 | for _, f := range fs { 300 | f() 301 | } 302 | } 303 | 304 | // Pend skips all specs in the top-level group. 305 | // 306 | // All Options are ignored. 307 | func Pend(t *testing.T, text string, f func(*testing.T, G, S), _ ...Option) bool { 308 | t.Helper() 309 | return Run(t, text, f, func(c *config) { c.pend = true }) 310 | } 311 | 312 | // Focus focuses every spec in the provided suite. 313 | // This is useful as a shortcut for unfocusing all focused specs. 314 | // 315 | // Valid Options: 316 | // Sequential, Random, Reverse, Parallel 317 | // Local, Global, Flat, Nested 318 | // Seed, Report 319 | func Focus(t *testing.T, text string, f func(*testing.T, G, S), opts ...Option) bool { 320 | t.Helper() 321 | return Run(t, text, f, append(opts, func(c *config) { c.focus = true })...) 322 | } 323 | 324 | // A Plan provides a Reporter with information about a suite. 325 | type Plan struct { 326 | Text string 327 | Total int 328 | Pending int 329 | Focused int 330 | Seed int64 331 | HasRandom bool 332 | HasFocus bool 333 | } 334 | 335 | // A Spec provides a Reporter with information about a spec immediately after 336 | // the spec completes. 337 | type Spec struct { 338 | Text []string 339 | Failed bool 340 | Skipped bool 341 | Focused bool 342 | Parallel bool 343 | Out io.Reader 344 | } 345 | 346 | // A Reporter is provided with information about a suite as it runs. 347 | type Reporter interface { 348 | 349 | // Start provides the Reporter with a Plan that describes the suite. 350 | // No specs will run until the Start method call finishes. 351 | Start(*testing.T, Plan) 352 | 353 | // Specs provides the Reporter with a channel of Specs. 354 | // The specs will start running concurrently with the Specs method call. 355 | // The Run method will not complete until the Specs method call completes. 356 | Specs(*testing.T, <-chan Spec) 357 | } 358 | -------------------------------------------------------------------------------- /spec_test.go: -------------------------------------------------------------------------------- 1 | package spec_test 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/sclevine/spec" 9 | "github.com/sclevine/spec/report" 10 | ) 11 | 12 | type recorder func(*testing.T, string) func() 13 | 14 | func record(t *testing.T) (s recorder, c func() []string) { 15 | var calls []string 16 | return func(ts *testing.T, id string) func() { 17 | return func() { 18 | if ts == nil { 19 | t.Fatal("Spec running during parse phase for:", t.Name()) 20 | } 21 | name := strings.TrimPrefix(ts.Name(), t.Name()+"/") + "->" + id 22 | calls = append(calls, name) 23 | } 24 | }, func() []string { 25 | return calls 26 | } 27 | } 28 | 29 | func specTestCases(t *testing.T, when spec.G, it spec.S, s recorder) { 30 | it.Before(s(t, "Before")) 31 | it.After(s(t, "After")) 32 | 33 | it("S", s(t, "S")) 34 | it.Pend("S.Pend", s(t, "S.Pend")) 35 | it.Focus("S.Focus", s(t, "S.Focus")) 36 | 37 | when("G", func() { 38 | it.Before(s(t, "G.Before")) 39 | it.After(s(t, "G.After")) 40 | it("G.S", s(t, "G.S")) 41 | }) 42 | when.Pend("G.Pend", func() { 43 | it.Before(s(t, "G.Pend.Before")) 44 | it.After(s(t, "G.Pend.After")) 45 | it("G.Pend.S", s(t, "G.Pend.S")) 46 | }) 47 | when.Focus("G.Focus", func() { 48 | it.Before(s(t, "G.Focus.Before")) 49 | it.After(s(t, "G.Focus.After")) 50 | it("G.Focus.S", s(t, "G.Focus.S")) 51 | }) 52 | } 53 | 54 | func TestRun(t *testing.T) { 55 | s, calls := record(t) 56 | 57 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 58 | specTestCases(t, when, it, s) 59 | }) 60 | 61 | if !reflect.DeepEqual(calls(), []string{ 62 | "Run/S.Focus->Before", 63 | "Run/S.Focus->S.Focus", 64 | "Run/S.Focus->After", 65 | 66 | "Run/G.Focus/G.Focus.S->Before", "Run/G.Focus/G.Focus.S->G.Focus.Before", 67 | "Run/G.Focus/G.Focus.S->G.Focus.S", 68 | "Run/G.Focus/G.Focus.S->G.Focus.After", "Run/G.Focus/G.Focus.S->After", 69 | }) { 70 | t.Fatal("Incorrect order:", calls()) 71 | } 72 | } 73 | 74 | func TestSuiteRun(t *testing.T) { 75 | s, calls := record(t) 76 | 77 | suite := spec.New("Suite") 78 | suite("Top.1", func(t *testing.T, when spec.G, it spec.S) { 79 | specTestCases(t, when, it, s) 80 | }) 81 | suite("Top.2", func(t *testing.T, when spec.G, it spec.S) { 82 | it.Focus("Top.2.S.Focus", s(t, "Top.2.S.Focus")) 83 | }) 84 | suite.Run(t) 85 | 86 | if !reflect.DeepEqual(calls(), []string{ 87 | "Suite/Top.1/S.Focus->Before", 88 | "Suite/Top.1/S.Focus->S.Focus", 89 | "Suite/Top.1/S.Focus->After", 90 | 91 | "Suite/Top.1/G.Focus/G.Focus.S->Before", "Suite/Top.1/G.Focus/G.Focus.S->G.Focus.Before", 92 | "Suite/Top.1/G.Focus/G.Focus.S->G.Focus.S", 93 | "Suite/Top.1/G.Focus/G.Focus.S->G.Focus.After", "Suite/Top.1/G.Focus/G.Focus.S->After", 94 | 95 | "Suite/Top.2/Top.2.S.Focus->Top.2.S.Focus", 96 | }) { 97 | t.Fatal("Incorrect order:", calls()) 98 | } 99 | } 100 | 101 | func TestPend(t *testing.T) { 102 | s, calls := record(t) 103 | 104 | spec.Pend(t, "Pend", func(t *testing.T, when spec.G, it spec.S) { 105 | specTestCases(t, when, it, s) 106 | }) 107 | 108 | if len(calls()) != 0 { 109 | t.Fatal("Failed to pend:", calls()) 110 | } 111 | } 112 | 113 | func TestSuitePend(t *testing.T) { 114 | s, calls := record(t) 115 | 116 | suite := spec.New("Suite") 117 | suite.Pend("Top.Pend", func(t *testing.T, when spec.G, it spec.S) { 118 | specTestCases(t, when, it, s) 119 | }) 120 | suite.Run(t) 121 | 122 | if len(calls()) != 0 { 123 | t.Fatal("Failed to pend:", calls()) 124 | } 125 | } 126 | 127 | func TestGPend(t *testing.T) { 128 | s, calls := record(t) 129 | 130 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 131 | when.Pend("Run.G.Pend", func() { 132 | specTestCases(t, when, it, s) 133 | }) 134 | }) 135 | 136 | if len(calls()) != 0 { 137 | t.Fatal("Failed to pend:", calls()) 138 | } 139 | } 140 | 141 | func TestSPend(t *testing.T) { 142 | s, calls := record(t) 143 | 144 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 145 | it.Pend("Run.S", s(t, "Run.S")) 146 | }) 147 | 148 | if len(calls()) != 0 { 149 | t.Fatal("Failed to pend:", calls()) 150 | } 151 | } 152 | 153 | func TestFocus(t *testing.T) { 154 | s, calls := record(t) 155 | 156 | spec.Focus(t, "Focus", func(t *testing.T, when spec.G, it spec.S) { 157 | specTestCases(t, when, it, s) 158 | }) 159 | 160 | if !reflect.DeepEqual(calls(), []string{ 161 | "Focus/S->Before", 162 | "Focus/S->S", 163 | "Focus/S->After", 164 | 165 | "Focus/S.Focus->Before", 166 | "Focus/S.Focus->S.Focus", 167 | "Focus/S.Focus->After", 168 | 169 | "Focus/G/G.S->Before", "Focus/G/G.S->G.Before", 170 | "Focus/G/G.S->G.S", 171 | "Focus/G/G.S->G.After", "Focus/G/G.S->After", 172 | 173 | "Focus/G.Focus/G.Focus.S->Before", "Focus/G.Focus/G.Focus.S->G.Focus.Before", 174 | "Focus/G.Focus/G.Focus.S->G.Focus.S", 175 | "Focus/G.Focus/G.Focus.S->G.Focus.After", "Focus/G.Focus/G.Focus.S->After", 176 | }) { 177 | t.Fatal("Incorrect order:", calls()) 178 | } 179 | } 180 | 181 | func TestSuiteFocus(t *testing.T) { 182 | s, calls := record(t) 183 | 184 | suite := spec.New("Suite") 185 | suite.Focus("Top.Focus", func(t *testing.T, when spec.G, it spec.S) { 186 | specTestCases(t, when, it, s) 187 | }) 188 | suite.Run(t) 189 | 190 | if !reflect.DeepEqual(calls(), []string{ 191 | "Suite/Top.Focus/S->Before", 192 | "Suite/Top.Focus/S->S", 193 | "Suite/Top.Focus/S->After", 194 | 195 | "Suite/Top.Focus/S.Focus->Before", 196 | "Suite/Top.Focus/S.Focus->S.Focus", 197 | "Suite/Top.Focus/S.Focus->After", 198 | 199 | "Suite/Top.Focus/G/G.S->Before", "Suite/Top.Focus/G/G.S->G.Before", 200 | "Suite/Top.Focus/G/G.S->G.S", 201 | "Suite/Top.Focus/G/G.S->G.After", "Suite/Top.Focus/G/G.S->After", 202 | 203 | "Suite/Top.Focus/G.Focus/G.Focus.S->Before", "Suite/Top.Focus/G.Focus/G.Focus.S->G.Focus.Before", 204 | "Suite/Top.Focus/G.Focus/G.Focus.S->G.Focus.S", 205 | "Suite/Top.Focus/G.Focus/G.Focus.S->G.Focus.After", "Suite/Top.Focus/G.Focus/G.Focus.S->After", 206 | }) { 207 | t.Fatal("Incorrect order:", calls()) 208 | } 209 | } 210 | 211 | func TestGFocus(t *testing.T) { 212 | s, calls := record(t) 213 | 214 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 215 | when.Focus("Run.G.Focus", func() { 216 | specTestCases(t, when, it, s) 217 | }) 218 | }) 219 | 220 | if !reflect.DeepEqual(calls(), []string{ 221 | "Run/Run.G.Focus/S->Before", 222 | "Run/Run.G.Focus/S->S", 223 | "Run/Run.G.Focus/S->After", 224 | 225 | "Run/Run.G.Focus/S.Focus->Before", 226 | "Run/Run.G.Focus/S.Focus->S.Focus", 227 | "Run/Run.G.Focus/S.Focus->After", 228 | 229 | "Run/Run.G.Focus/G/G.S->Before", "Run/Run.G.Focus/G/G.S->G.Before", 230 | "Run/Run.G.Focus/G/G.S->G.S", 231 | "Run/Run.G.Focus/G/G.S->G.After", "Run/Run.G.Focus/G/G.S->After", 232 | 233 | "Run/Run.G.Focus/G.Focus/G.Focus.S->Before", "Run/Run.G.Focus/G.Focus/G.Focus.S->G.Focus.Before", 234 | "Run/Run.G.Focus/G.Focus/G.Focus.S->G.Focus.S", 235 | "Run/Run.G.Focus/G.Focus/G.Focus.S->G.Focus.After", "Run/Run.G.Focus/G.Focus/G.Focus.S->After", 236 | }) { 237 | t.Fatal("Incorrect order:", calls()) 238 | } 239 | } 240 | 241 | func TestSFocus(t *testing.T) { 242 | s, calls := record(t) 243 | 244 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 245 | it.Focus("Run.S.Focus", s(t, "Run.S.Focus")) 246 | }) 247 | 248 | if !reflect.DeepEqual(calls(), []string{ 249 | "Run/Run.S.Focus->Run.S.Focus", 250 | }) { 251 | t.Fatal("Incorrect order:", calls()) 252 | } 253 | } 254 | 255 | func TestSBefore(t *testing.T) { 256 | s, calls := record(t) 257 | 258 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 259 | it.Before(s(t, "Run.Before.1")) 260 | it("Run.S", s(t, "Run.S")) 261 | it.Before(s(t, "Run.Before.2")) 262 | when("Run.G", func() { 263 | it.Before(s(t, "Run.G.Before.1")) 264 | it("Run.G.S", s(t, "Run.G.S")) 265 | it.Before(s(t, "Run.G.Before.2")) 266 | }) 267 | it.Before(s(t, "Run.Before.3")) 268 | }) 269 | 270 | if !reflect.DeepEqual(calls(), []string{ 271 | "Run/Run.S->Run.Before.1", "Run/Run.S->Run.Before.2", "Run/Run.S->Run.Before.3", 272 | "Run/Run.S->Run.S", 273 | 274 | "Run/Run.G/Run.G.S->Run.Before.1", "Run/Run.G/Run.G.S->Run.Before.2", "Run/Run.G/Run.G.S->Run.Before.3", 275 | "Run/Run.G/Run.G.S->Run.G.Before.1", "Run/Run.G/Run.G.S->Run.G.Before.2", 276 | "Run/Run.G/Run.G.S->Run.G.S", 277 | }) { 278 | t.Fatal("Incorrect order:", calls()) 279 | } 280 | } 281 | 282 | func TestSuiteBefore(t *testing.T) { 283 | s, calls := record(t) 284 | 285 | suite := spec.New("Suite") 286 | suite.Before(func(t *testing.T) { 287 | s(t, "Before.1")() 288 | }) 289 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 290 | it.Before(s(t, "Top.Before")) 291 | when("Top.G", func() { 292 | it.Before(s(t, "Top.G.Before")) 293 | it("Top.G.S", s(t, "Top.G.S")) 294 | }) 295 | }) 296 | suite.Before(func(t *testing.T) { 297 | s(t, "Before.2")() 298 | }) 299 | suite.Run(t) 300 | 301 | if !reflect.DeepEqual(calls(), []string{ 302 | "Suite/Top/Top.G/Top.G.S->Before.1", "Suite/Top/Top.G/Top.G.S->Before.2", 303 | "Suite/Top/Top.G/Top.G.S->Top.Before", 304 | "Suite/Top/Top.G/Top.G.S->Top.G.Before", 305 | "Suite/Top/Top.G/Top.G.S->Top.G.S", 306 | }) { 307 | t.Fatal("Incorrect order:", calls()) 308 | } 309 | } 310 | 311 | func TestSAfter(t *testing.T) { 312 | s, calls := record(t) 313 | 314 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 315 | it.After(s(t, "Run.After.1")) 316 | it("Run.S", s(t, "Run.S")) 317 | it.After(s(t, "Run.After.2")) 318 | when("Run.G", func() { 319 | it.After(s(t, "Run.G.After.1")) 320 | it("Run.G.S", s(t, "Run.G.S")) 321 | it.After(s(t, "Run.G.After.2")) 322 | }) 323 | it.After(s(t, "Run.After.3")) 324 | }) 325 | 326 | if !reflect.DeepEqual(calls(), []string{ 327 | "Run/Run.S->Run.S", 328 | "Run/Run.S->Run.After.1", "Run/Run.S->Run.After.2", "Run/Run.S->Run.After.3", 329 | 330 | "Run/Run.G/Run.G.S->Run.G.S", 331 | "Run/Run.G/Run.G.S->Run.G.After.1", "Run/Run.G/Run.G.S->Run.G.After.2", 332 | "Run/Run.G/Run.G.S->Run.After.1", "Run/Run.G/Run.G.S->Run.After.2", "Run/Run.G/Run.G.S->Run.After.3", 333 | }) { 334 | t.Fatal("Incorrect order:", calls()) 335 | } 336 | } 337 | 338 | func TestSuiteAfter(t *testing.T) { 339 | s, calls := record(t) 340 | 341 | suite := spec.New("Suite") 342 | suite.After(func(t *testing.T) { 343 | s(t, "After.1")() 344 | }) 345 | suite("Top", func(t *testing.T, when spec.G, it spec.S) { 346 | it.After(s(t, "Top.After")) 347 | when("Top.G", func() { 348 | it.After(s(t, "Top.G.After")) 349 | it("Top.G.S", s(t, "Top.G.S")) 350 | }) 351 | }) 352 | suite.After(func(t *testing.T) { 353 | s(t, "After.2")() 354 | }) 355 | suite.Run(t) 356 | 357 | if !reflect.DeepEqual(calls(), []string{ 358 | "Suite/Top/Top.G/Top.G.S->Top.G.S", 359 | "Suite/Top/Top.G/Top.G.S->Top.G.After", 360 | "Suite/Top/Top.G/Top.G.S->Top.After", 361 | "Suite/Top/Top.G/Top.G.S->After.1", "Suite/Top/Top.G/Top.G.S->After.2", 362 | }) { 363 | t.Fatal("Incorrect order:", calls()) 364 | } 365 | } 366 | 367 | func TestSkipAfter(t *testing.T) { 368 | s, calls := record(t) 369 | 370 | spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { 371 | it.Before(func() { 372 | s(t, "Run.Before")() 373 | t.SkipNow() 374 | }) 375 | it.After(s(t, "Run.After")) 376 | when("Run.G", func() { 377 | it.After(s(t, "Run.G.After")) 378 | it("Run.G.S", s(t, "Run.G.S")) 379 | }) 380 | }) 381 | 382 | if !reflect.DeepEqual(calls(), []string{ 383 | "Run/Run.G/Run.G.S->Run.Before", 384 | "Run/Run.G/Run.G.S->Run.After", 385 | }) { 386 | t.Fatal("Incorrect order:", calls()) 387 | } 388 | } 389 | 390 | func TestSpec(t *testing.T) { 391 | spec.Run(t, "spec", func(t *testing.T, when spec.G, it spec.S) { 392 | when("something happens", func() { 393 | var someStr string 394 | 395 | it.Before(func() { 396 | t.Log("before") 397 | if someStr == "some-value" { 398 | t.Fatal("test pollution") 399 | } 400 | someStr = "some-value" 401 | }) 402 | 403 | it.After(func() { 404 | t.Log("after") 405 | }) 406 | 407 | it("should do something", func() { 408 | t.Log("first") 409 | }) 410 | 411 | when("something else also happens", func() { 412 | it.Before(func() { 413 | t.Log("nested before") 414 | }) 415 | 416 | it("should do something nested", func() { 417 | t.Log("second") 418 | }) 419 | 420 | it.After(func() { 421 | t.Log("nested after") 422 | }) 423 | }) 424 | 425 | when("some things happen in parallel at the end", func() { 426 | it.After(func() { 427 | t.Log("lone after") 428 | }) 429 | 430 | it("should do one thing in parallel", func() { 431 | t.Log("first parallel") 432 | }) 433 | 434 | it("should do another thing in parallel", func() { 435 | t.Log("second parallel") 436 | }) 437 | }, spec.Parallel()) 438 | 439 | when("some things happen randomly", func() { 440 | it.Before(func() { 441 | t.Log("before random") 442 | }) 443 | 444 | it("should do one thing randomly", func() { 445 | t.Log("first random") 446 | }) 447 | 448 | it("should do another thing randomly", func() { 449 | t.Log("second random") 450 | }) 451 | }, spec.Random()) 452 | 453 | when("some things happen in reverse and in nested subtests", func() { 454 | it.Before(func() { 455 | t.Log("before reverse") 456 | }) 457 | 458 | it("should do another thing second", func() { 459 | t.Log("second reverse") 460 | }) 461 | 462 | it("should do one thing first", func() { 463 | t.Log("first reverse") 464 | }) 465 | }, spec.Reverse(), spec.Nested()) 466 | 467 | when("some things happen in globally random order", func() { 468 | it.Before(func() { 469 | t.Log("before global") 470 | }) 471 | 472 | when("grouped first", func() { 473 | it.Before(func() { 474 | t.Log("before group one global") 475 | }) 476 | 477 | it("should do one thing in group one randomly", func() { 478 | t.Log("group one, spec one, global random") 479 | }) 480 | 481 | it("should do another thing in group one randomly", func() { 482 | t.Log("group one, spec two, global random") 483 | }) 484 | }) 485 | 486 | when("grouped second", func() { 487 | it.Before(func() { 488 | t.Log("before group two global") 489 | }) 490 | 491 | it("should do one thing in group two randomly", func() { 492 | t.Log("group two, spec one, global random") 493 | }) 494 | 495 | it("should do another thing in group two randomly", func() { 496 | t.Log("group two, spec two, global random") 497 | }) 498 | }, spec.Local()) 499 | 500 | it("should do one thing ungrouped", func() { 501 | t.Log("ungrouped global random") 502 | }) 503 | }, spec.Random(), spec.Global()) 504 | 505 | it("should do something else", func() { 506 | t.Log("third") 507 | }) 508 | 509 | it.Pend("should not do this", func() { 510 | t.Log("forth") 511 | }) 512 | 513 | when.Pend("nothing important happens", func() { 514 | it.Focus("should not really focus on this", func() { 515 | t.Log("fifth") 516 | }) 517 | }) 518 | }) 519 | }, spec.Report(report.Terminal{})) 520 | } 521 | --------------------------------------------------------------------------------