1 people like it.

Easy string approvals testing

This simple tool is useful for string based approvals testing. When tests are run in DEBUG mode, the code opens the p4merge diff tool (change it to point to yours) to clearly show the differences between the expected and actual strings. The actual, or received string is copied, in escaped form, to the clipboard so that it can be easily pasted into the associated test to approve the changes if appropriate. When a debugger isn't attached, this tool reverts to using a standard Assert so that it won't attempt to open a diff viewer when executed on a CI server.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
53: 
54: 
55: 
56: 
57: 
58: 
59: 
60: 
61: 
62: 
63: 
64: 
65: 
66: 
67: 
68: 
69: 
70: 
71: 
72: 
73: 
74: 
75: 
76: 
77: 
78: 
79: 
80: 
81: 
82: 
83: 
84: 
85: 
86: 
87: 
88: 
89: 
90: 
91: 
92: 
module Assertions

open System
open System.IO
open System.Diagnostics
open System.Windows
open Microsoft.VisualStudio.TestTools.UnitTesting    

/// Opens a set of files in a diff viewer.
/// Change to suit your diff viewer
let openDiff file1 file2 =
    let startInfo = new ProcessStartInfo()
    startInfo.FileName <- @"C:\Program Files\Perforce\p4merge.exe"
    startInfo.Arguments <- sprintf "%s %s" file1 file2
    Process.Start(startInfo);

/// Useable tempfile (see: http://fssnip.net/4N/)
type TempFile() =
     let path = System.IO.Path.GetTempFileName()
     member x.Path = path
     interface System.IDisposable with
         member x.Dispose() = System.IO.File.Delete(path)

/// Predicate for matching strings.
let stringsDiffer s1 s2 = 
    not (s1 = s2)

/// Uses a tester function to determine 
/// if the given strings, s1 and s2 match.
let testStrings' testFails expected actual  = 

    let diffMessage = 
        Environment.NewLine + "-------------------------" + 
        Environment.NewLine + 
        "This string has been copied to the clipboard." + Environment.NewLine + 
        "To approve, simply paste it into your test."
    
    // Escape this to create a verabtim F# string that
    // may be pasted directly into the test code.
    let buildEscapedStringForClipboard (str:string) = 
        let doubleQuotesEscaped = str.Replace(@"""", @"""""")
        sprintf @"@""%s""" doubleQuotesEscaped

    let copyToClipboard str = 
        try 
            System.Windows.Clipboard.SetData(
                DataFormats.Text, 
                buildEscapedStringForClipboard str);
        with 
            | ex -> () // swallow any COM exceptions here. ... 

    // True if a debugger is attached to the 
    // process running the tests.
    let debuggerIsAttached =                 
        Debugger.IsAttached

    if not debuggerIsAttached then
        Assert.AreEqual(expected, actual) // Assume running on CI server.
    else
        if testFails expected actual
        then // Show any change in diff viewer and copy received to clipboard.
            use tempFileExpected = new TempFile()
            use tempFileActual   = new TempFile()
            let tempFiles = [| tempFileExpected.Path; tempFileActual.Path |] 
            [| expected; actual + diffMessage |] 
            |> Array.zip tempFiles 
            |> Array.iter (fun tpl -> File.WriteAllText((fst tpl), (snd tpl)))
            openDiff tempFileExpected.Path tempFileActual.Path |> ignore
            actual |> copyToClipboard 
            failwith (sprintf "Expected <%s>, but actual is <%s>." expected actual)

/// Tests the given strings for equality.
/// If they're found to differ, then the 
/// p4merge tools is opened to show the 
/// differences.  The received string is 
/// escaped and sent to the clipboard 
/// so that it may be easily copied to the 
/// relvant test to accept the changes if 
/// they're expected.
[<DebuggerStepThrough>]        
let IsSameStringAs = testStrings' stringsDiffer

//--------------------------------------------------------
[<TestClass>]
type ``When Testing Using Strings``() =

    // Sample unit test showing how to use the 
    // simple string approvals tool.
    [<TestMethod>]        
    member this.``should be able to compare strings``() =
        "Actual String" // created by unit under test.
        |> IsSameStringAs @"Expected String"
module Assertions
namespace System
namespace System.IO
namespace System.Diagnostics
namespace System.Windows
namespace Microsoft
val openDiff : file1:string -> file2:string -> Process

Full name: Assertions.openDiff


 Opens a set of files in a diff viewer.
 Change to suit your diff viewer
val file1 : string
val file2 : string
val startInfo : ProcessStartInfo
Multiple items
type ProcessStartInfo =
  new : unit -> ProcessStartInfo + 2 overloads
  member Arguments : string with get, set
  member CreateNoWindow : bool with get, set
  member Domain : string with get, set
  member EnvironmentVariables : StringDictionary
  member ErrorDialog : bool with get, set
  member ErrorDialogParentHandle : nativeint with get, set
  member FileName : string with get, set
  member LoadUserProfile : bool with get, set
  member Password : SecureString with get, set
  ...

Full name: System.Diagnostics.ProcessStartInfo

--------------------
ProcessStartInfo() : unit
ProcessStartInfo(fileName: string) : unit
ProcessStartInfo(fileName: string, arguments: string) : unit
property ProcessStartInfo.FileName: string
property ProcessStartInfo.Arguments: string
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
Multiple items
type Process =
  inherit Component
  new : unit -> Process
  member BasePriority : int
  member BeginErrorReadLine : unit -> unit
  member BeginOutputReadLine : unit -> unit
  member CancelErrorRead : unit -> unit
  member CancelOutputRead : unit -> unit
  member Close : unit -> unit
  member CloseMainWindow : unit -> bool
  member EnableRaisingEvents : bool with get, set
  member ExitCode : int
  ...

Full name: System.Diagnostics.Process

--------------------
Process() : unit
Process.Start(startInfo: ProcessStartInfo) : Process
Process.Start(fileName: string) : Process
Process.Start(fileName: string, arguments: string) : Process
Process.Start(fileName: string, userName: string, password: Security.SecureString, domain: string) : Process
Process.Start(fileName: string, arguments: string, userName: string, password: Security.SecureString, domain: string) : Process
Multiple items
type TempFile =
  interface IDisposable
  new : unit -> TempFile
  member Path : string

Full name: Assertions.TempFile


 Useable tempfile (see: http://fssnip.net/4N/)


--------------------
new : unit -> TempFile
val path : string
type Path =
  static val DirectorySeparatorChar : char
  static val AltDirectorySeparatorChar : char
  static val VolumeSeparatorChar : char
  static val InvalidPathChars : char[]
  static val PathSeparator : char
  static member ChangeExtension : path:string * extension:string -> string
  static member Combine : [<ParamArray>] paths:string[] -> string + 3 overloads
  static member GetDirectoryName : path:string -> string
  static member GetExtension : path:string -> string
  static member GetFileName : path:string -> string
  ...

Full name: System.IO.Path
Path.GetTempFileName() : string
val x : TempFile
Multiple items
member TempFile.Path : string

Full name: Assertions.TempFile.Path

--------------------
type Path =
  static val DirectorySeparatorChar : char
  static val AltDirectorySeparatorChar : char
  static val VolumeSeparatorChar : char
  static val InvalidPathChars : char[]
  static val PathSeparator : char
  static member ChangeExtension : path:string * extension:string -> string
  static member Combine : [<ParamArray>] paths:string[] -> string + 3 overloads
  static member GetDirectoryName : path:string -> string
  static member GetExtension : path:string -> string
  static member GetFileName : path:string -> string
  ...

Full name: System.IO.Path
type IDisposable =
  member Dispose : unit -> unit

Full name: System.IDisposable
override TempFile.Dispose : unit -> unit

Full name: Assertions.TempFile.Dispose
type File =
  static member AppendAllLines : path:string * contents:IEnumerable<string> -> unit + 1 overload
  static member AppendAllText : path:string * contents:string -> unit + 1 overload
  static member AppendText : path:string -> StreamWriter
  static member Copy : sourceFileName:string * destFileName:string -> unit + 1 overload
  static member Create : path:string -> FileStream + 3 overloads
  static member CreateText : path:string -> StreamWriter
  static member Decrypt : path:string -> unit
  static member Delete : path:string -> unit
  static member Encrypt : path:string -> unit
  static member Exists : path:string -> bool
  ...

Full name: System.IO.File
File.Delete(path: string) : unit
val stringsDiffer : s1:'a -> s2:'a -> bool (requires equality)

Full name: Assertions.stringsDiffer


 Predicate for matching strings.
val s1 : 'a (requires equality)
val s2 : 'a (requires equality)
val not : value:bool -> bool

Full name: Microsoft.FSharp.Core.Operators.not
val testStrings' : testFails:(string -> string -> bool) -> expected:string -> actual:string -> unit

Full name: Assertions.testStrings'


 Uses a tester function to determine
 if the given strings, s1 and s2 match.
val testFails : (string -> string -> bool)
val expected : string
val actual : string
val diffMessage : string
type Environment =
  static member CommandLine : string
  static member CurrentDirectory : string with get, set
  static member Exit : exitCode:int -> unit
  static member ExitCode : int with get, set
  static member ExpandEnvironmentVariables : name:string -> string
  static member FailFast : message:string -> unit + 1 overload
  static member GetCommandLineArgs : unit -> string[]
  static member GetEnvironmentVariable : variable:string -> string + 1 overload
  static member GetEnvironmentVariables : unit -> IDictionary + 1 overload
  static member GetFolderPath : folder:SpecialFolder -> string + 1 overload
  ...
  nested type SpecialFolder
  nested type SpecialFolderOption

Full name: System.Environment
property Environment.NewLine: string
val buildEscapedStringForClipboard : (string -> string)
val str : string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
val doubleQuotesEscaped : string
String.Replace(oldValue: string, newValue: string) : string
String.Replace(oldChar: char, newChar: char) : string
val copyToClipboard : ('a -> unit)
val str : 'a
namespace System.Text
val ex : exn
val debuggerIsAttached : bool
type Debugger =
  new : unit -> Debugger
  static val DefaultCategory : string
  static member Break : unit -> unit
  static member IsAttached : bool
  static member IsLogging : unit -> bool
  static member Launch : unit -> bool
  static member Log : level:int * category:string * message:string -> unit
  static member NotifyOfCrossThreadDependency : unit -> unit

Full name: System.Diagnostics.Debugger
property Debugger.IsAttached: bool
val tempFileExpected : TempFile
val tempFileActual : TempFile
val tempFiles : string []
property TempFile.Path: string
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
val zip : array1:'T1 [] -> array2:'T2 [] -> ('T1 * 'T2) []

Full name: Microsoft.FSharp.Collections.Array.zip
val iter : action:('T -> unit) -> array:'T [] -> unit

Full name: Microsoft.FSharp.Collections.Array.iter
val tpl : string * string
File.WriteAllText(path: string, contents: string) : unit
File.WriteAllText(path: string, contents: string, encoding: Text.Encoding) : unit
val fst : tuple:('T1 * 'T2) -> 'T1

Full name: Microsoft.FSharp.Core.Operators.fst
val snd : tuple:('T1 * 'T2) -> 'T2

Full name: Microsoft.FSharp.Core.Operators.snd
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
val failwith : message:string -> 'T

Full name: Microsoft.FSharp.Core.Operators.failwith
Multiple items
type DebuggerStepThroughAttribute =
  inherit Attribute
  new : unit -> DebuggerStepThroughAttribute

Full name: System.Diagnostics.DebuggerStepThroughAttribute

--------------------
DebuggerStepThroughAttribute() : unit
val IsSameStringAs : (string -> string -> unit)

Full name: Assertions.IsSameStringAs


 Tests the given strings for equality.
 If they're found to differ, then the
 p4merge tools is opened to show the
 differences. The received string is
 escaped and sent to the clipboard
 so that it may be easily copied to the
 relvant test to accept the changes if
 they're expected.
val this : When Testing Using Strings
member When Testing Using Strings.( should be able to compare strings ) : unit -> unit

Full name: Assertions.When Testing Using Strings.( should be able to compare strings )

More information

Link:http://fssnip.net/d1
Posted:11 years ago
Author:Russell Politzky
Tags: unit testing , approvals testing