Skip to content

Refactor into vba-test #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 156 additions & 126 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
VBA-TDD
=======
# vba-test

Bring the reliability of other programming realms to VBA with Test-Driven Development (TDD) for VBA on Windows and Mac.
Add testing to VBA on Windows and Mac.

Quick example:
## Example

```vb
Function Specs() As SpecSuite
Set Specs = New SpecSuite
Specs.Description = "Add"

' Report results to the Immediate Window
' (ctrl + g or View > Immediate Window)
Dim Reporter As New ImmediateReporter
Reporter.ListenTo Specs

' Describe the desired behavior
With Specs.It("should add two numbers")
' Test the desired behavior
.Expect(Add(2, 2)).ToEqual 4
.Expect(Add(3, -1)).ToEqual 2
.Expect(Add(-1, -2)).ToEqual -3
End With

With Specs.It("should add any number of numbers")
.Expect(Add(1, 2, 3)).ToEqual 6
.Expect(Add(1, 2, 3, 4)).ToEqual 10
End With
End Function
Function AddTests() As TestSuite
Set AddTests = New TestSuite
AddTests.Description = "Add"

' Report results to the Immediate Window
' (ctrl + g or View > Immediate Window)
Dim Reporter As New ImmediateReporter
Reporter.ListenTo AddTests

With AddTests.Test("should add two numbers")
.IsEqual Add(2, 2), 4
.IsEqual Add(3, -1), 2
.IsEqual Add(-1, -2), -3
End With

With AddTests.Test("should add any number of numbers")
.IsEqual Add(1, 2, 3), 6
.IsEqual Add(1, 2, 3, 4), 10
End With
End Sub

Public Function Add(ParamArray Values() As Variant) As Double
Dim i As Integer
Add = 0
For i = LBound(Values) To UBound(Values)
Add = Add + Values(i)
Next i
Dim i As Integer
Add = 0

For i = LBound(Values) To UBound(Values)
Add = Add + Values(i)
Next i
End Function

' Immediate Window:
Expand All @@ -48,131 +45,164 @@ End Function

For details of the process of reaching this example, see the [TDD Example](https://github.com/VBA-tools/VBA-TDD/wiki/TDD-Example)

### Advanced Example
## Advanced Example

For an advanced example of what is possible with VBA-TDD, check out the [specs for VBA-Web](https://github.com/VBA-tools/VBA-Web/tree/master/specs)
For an advanced example of what is possible with vba-test, check out the [tests for VBA-Web](https://github.com/VBA-tools/VBA-Web/tree/master/specs)

### Getting Started
## Getting Started

1. Download the [latest release (v2.0.0-beta)](https://github.com/VBA-tools/VBA-TDD/releases)
2. Add `src/SpecSuite.cls`, `src/SpecDefinition.cls`, `src/SpecExpectation.cls`, add `src/ImmediateReporter.cls` to your project
3. If you're starting from scratch with Excel, you can use `VBA-TDD - Blank.xlsm`
1. Download the [latest release (v2.0.0-beta.2)](https://github.com/vba-tools/vba-test/releases)
2. Add `src/TestSuite.cls`, `src/TestCase.cls`, add `src/ImmediateReporter.cls` to your project
3. If you're starting from scratch with Excel, you can use `vba-test-blank.xlsm`

### It and Expect
## TestSuite

`It` is how you describe desired behavior and once a collection of specs is written, it should read like a list of requirements.
A test suite groups tests together, runs test hooks for actions that should be run before and after tests, and is responsible for passing test results to reporters.

```vb
With Specs.It("should allow user to continue if they are authorized and up-to-date")
' ...
End With

With Specs.It("should show an X when the user rolls a strike")
' ...
' Create a new test suite
Dim Suite As New TestSuite
Suite.Description = "Module Name"

' Create a new test
Dim Test As TestCase
Set Test = Suite.Test("Test Name")
Test.IsEqual ' ...

' or create and use test using With
With Suite.Test("Test Name")
.IsEqual '...
End With
```

`Expect` is how you test desired behavior
__TestSuite API__

- `Description`
- `Test(Name) As TestCase`
- _Event_ `BeforeEach(Test)`
- _Event_ `Result(Test)`
- _Event_ `AfterEach(Test)`

## TestCase

A test case uses assertions to test a specific part of your application.

```vb
With Specs.It("should check values")
.Expect(2 + 2).ToEqual 4
.Expect(2 + 2).ToNotEqual 5
.Expect(2 + 2).ToBeLessThan 7
.Expect(2 + 2).ToBeLT 6
.Expect(2 + 2).ToBeLessThanOrEqualTo 5
.Expect(2 + 2).ToBeLTE 4
.Expect(2 + 2).ToBeGreaterThan 1
.Expect(2 + 2).ToBeGT 2
.Expect(2 + 2).ToBeGreaterThanOrEqualTo 3
.Expect(2 + 2).ToBeGTE 4
.Expect(2 + 2).ToBeCloseTo 3.9, 0
With Suite.Test("specific part of your application")
.IsEqual A, B, "(optional message, e.g. result should be 12)"
.NotEqual B, C

.IsOk C > B
.NotOk B > C

.IsUndefined ' Checks Nothing, Empty, Missing, or Null
.NotUndefined

.Includes Array(1, 2, 3), 2
.NotIncludes Array(1, 2, 3), 4
.IsApproximate 1.001, 1.002, 2
.NotApproximate 1.001, 1.009, 3

.Pass
.Fail "e.g. should not have gotten here"
.Plan 4 ' Should only be 4 assertions, more or less fails
.Skip ' skip this test
End With

With Specs.It("should check Nothing, Empty, Missing, and Null")
.Expect(Nothing).ToBeNothing
.Expect(Empty).ToBeEmpty
.Expect().ToBeMissing
.Expect(Null).ToBeNull

' `ToBeUndefined` checks if it's Nothing or Empty or Missing or Null

.Expect(Nothing).ToBeUndefined
.Expect(Empty).ToBeUndefined
.Expect().ToBeUndefined
.Expect(Null).ToBeUndefined

' Classes are undefined until they are instantiated
Dim Sheet As Worksheet
.Expect(Sheet).ToBeNothing

.Expect("Howdy!").ToNotBeUndefined
.Expect(4).ToNotBeUndefined

Set Sheet = ThisWorkbook.Sheets(1)
.Expect(Sheet).ToNotBeUndefined
With Suite.Test("complex things")
.IsEqual _
ThisWorkbook.Sheets("Hidden").Visible, _
XlSheetVisibility.xlSheetVisible
.IsEqual _
ThisWorkbook.Sheets("Main").Cells(1, 1).Interior.Color, _
RGB(255, 0, 0)
End With
```

In addition to these basic assertions, custom assertions can be made by passing the `TestCase` to an assertion function

```vb
Sub ToBeWithin(Test As TestCase, Value As Variant, Min As Variant, Max As Variant)
Dim Message As String
Message = "Expected " & Value & " to be within " & Min & " and " & Max

Test.IsOk Value >= Min, Message
Test.IsOk Value <= Max, Message
End Sub

With Specs.It("should test complex things")
.Expect(ThisWorkbook.Sheets("Hidden").Visible).ToNotEqual XlSheetVisibility.xlSheetVisible
.Expect(ThisWorkbook.Sheets("Main").Cells(1, 1).Interior.Color).ToEqual RGB(255, 0, 0)
With Suite.Test("...")
ToBeWithin(.Self, Value, 0, 100)
End With
```

### ImmediateReporter
__TestCase API__

- `Test.Name`
- `Test.Self` - Reference to test case (useful inside of `With`)
- `Test.Context` - `Dictionary` holding test context (useful for `BeforeEach`/`AfterEach`)
- `Test.IsEqual(A, B, [Message])`
- `Test.NotEqual(A, B, [Message])`
- `Test.IsOk(Value, [Message])`
- `Test.NotOk(Value, [Message])`
- `Test.IsUndefined(Value, [Message])`
- `Test.NotUndefined(Value, [Message])`
- `Test.Includes(Values, Value, [Message])` - Check if value is included in array or `Collection`
- `Test.NotIncludes(Values, Value, [Message])`
- `Test.IsApproximate(A, B, SignificantFigures, [Message])` - Check if two values are close to each other (useful for `Double` values)
- `Test.NotApproximate(A, B, SignificantFigures, [Message])`
- `Test.Pass()` - Explicitly pass the test
- `Test.Fail([Message])` - Explicitly fail the test
- `Test.Plan(Count)` - For tests with loops and branches, it is important to catch if any assertions are skipped or extra
- `Test.Skip()` - Notify suite to skip this test

Generally, more advanced assertions should be added with custom assertions functions (detailed above), but there are common assertions that will be added (e.g. `IsApproximate` = close within significant fixtures, `Includes` = array/collection includes value, )

With your specs defined, the easiest way to display the test results is with `ImmediateReporter`. This outputs results to the Immediate Window (`ctrl+g` or View > Immediate Window) and is useful for running your tests without leaving the VBA editor.
## ImmediateReporter

With your tests defined, the easiest way to display the test results is with `ImmediateReporter`. This outputs results to the Immediate Window (`ctrl+g` or View > Immediate Window) and is useful for running your tests without leaving the VBA editor.

```vb
Public Function Specs As SpecSuite
Set Specs = New SpecSuite
Specs.Description = "..."
Public Function Suite As TestSuite
Set Suite = New TestSuite
Suite.Description = "..."

' Create reporter and attach it to these specs
Dim Reporter As New ImmediateReporter
Reporter.ListenTo Specs
' Create reporter and attach it to these specs
Dim Reporter As New ImmediateReporter
Reporter.ListenTo Suite

' -> Reporter will now output results as they are generated
' -> Reporter will now output results as they are generated
End Function
```

### RunMatcher
## Context / Lifecycle Hooks

For VBA applications that support `Application.Run` (which is at least Windows Excel, Word, and Access), you can create custom expect functions with `RunMatcher`.
`TestSuite` includes events for setup and teardown before tests and a `Context` object for passing values into tests that are properly torn down between tests.

```vb
Public Function Specs As SpecSuite
Set Specs = New SpecSuite

With Specs.It("should be within 1 and 100")
.Expect(50).RunMatcher "ToBeWithin", "to be within", 1, 100
' ^ Actual
' ^ Public Function to call
' ^ message for matcher
' ^ 0+ Args to pass to matcher
End With
End Function
' Class TestFixture
Private WithEvents pSuite As TestSuite

Public Function ToBeWithin(Actual As Variant, Args As Variant) As Variant
If UBound(Args) - LBound(Args) < 1 Then
' Return string for specific failure message
ToBeWithin = "Need to pass in upper-bound to ToBeWithin"
Else
If Actual >= Args(0) And Actual <= Args(1) Then
' Return true for pass
ToBeWithin = True
Else
' Return false for fail or custom failure message
ToBeWithin = False
End If
End If
End Function
```
Public Sub ListenTo(Suite As TestSuite)
Set pSuite = Suite
End Sub

Private Sub pSuite_BeforeEach(Test As TestCase)
Test.Context.Add "fixture", New Collection
End Sub

Private Sub pSuite_AfterEach(Test As TestCase)
' Context is cleared automatically,
' but can manually cleanup here
End Sub

To avoid compilation issues on unsupported applications, the compiler constant `EnableRunMatcher` in `SpecExpectation.cls` should be set to `False`.
' Elsewhere

For more details, check out the [Wiki](https://github.com/VBA-tools/VBA-TDD/wiki)
Dim Suite As New TestSuite

- Design based heavily on the [Jasmine](https://jasmine.github.io/)
- Author: Tim Hall
- License: MIT
Dim Fixture As New TestFixture
Fixture.ListenTo Suite

With Suite.Test("...")
.Context("fixture").Add "..."
End With
```
57 changes: 0 additions & 57 deletions specs/Specs_SpecDefinition.bas

This file was deleted.

Loading