2018-09-14-gotest-tools-fs.org (7625B)
1 #+TITLE: Golang testing — gotest.tools fs 2 #+date: <2018-09-14 Fri> 3 #+filetags: go testing fs 4 #+setupfile: ../templates/post.org 5 6 #+TOC: headlines 2 7 8 * Introduction 9 10 Let's continue the [[https://gotest.tools][=gotest.tools=]] serie, this time with the =fs= package. 11 12 #+BEGIN_QUOTE 13 Package fs provides tools for creating temporary files, and testing the contents and structure of a directory. 14 #+END_QUOTE 15 16 This package is heavily using functional arguments (as we saw in [[/posts/2017-01-01-go-testing-functionnal-builders/][functional arguments for 17 wonderful builders]]). Functional arguments is, in a nutshell, a combinaison of two Go 18 features : /variadic/ functions (=...= operation in a function signature) and the fact 19 that =func= are /first class citizen/. This looks more or less like that. 20 21 #+BEGIN_SRC go 22 type Config struct {} 23 24 func MyFn(ops ...func(*Config)) *Config { 25 c := &Config{} // with default values 26 for _, op := range ops { 27 op(c) 28 } 29 return c 30 } 31 32 // Calling it 33 conf := MyFn(withFoo, withBar("baz")) 34 #+END_SRC 35 36 The =fs= package has too *main* purpose : 37 38 1. create folders and files required for testing in a simple manner 39 2. compare two folders structure (and content) 40 41 * Create folder structures 42 43 Sometimes, you need to create folder structures (and files) in tests. Doing =i/o= work 44 takes time so try to limit the number of tests that needs to do that, especially in unit 45 tests. Doing it in tests adds a bit of boilerplate that could be avoid. As stated [[/posts/2017-01-01-go-testing-functionnal-builders/][before]] : 46 47 #+BEGIN_QUOTE 48 One of the most important characteristic of a unit test (and any type of test really) is 49 *readability*. This means it should be easy to read but most importantly it should *clearly 50 show the intent* of the test. The setup (and cleanup) of the tests should be as small as 51 possible to avoid the noise. 52 #+END_QUOTE 53 54 In a test you usually end up using =ioutil= function to create what you need. This looks 55 somewhat like the following. 56 57 #+BEGIN_SRC go 58 path, err := ioutil.TempDir("", "bar") 59 if err != nil { // or using `assert.Assert` 60 t.Fatal(err) 61 } 62 if err := os.Mkdir(filepath.Join(path, "foo"), os.FileMode(0755)); err != nil { 63 t.Fatal(err) 64 } 65 if err := ioutil.WriteFile(filepath.Join(path, "foo", "bar"), []byte("content"), os.FileMode(0777)); err != nil { 66 t.Fatal(err) 67 } 68 defer os.RemoveAll(path) // to clean up at the end of the test 69 #+END_SRC 70 71 The =fs= package intends to help reduce the noise and comes with a bunch function to create 72 folder structure : 73 74 - two main function =NewFile= and =NewDir= 75 - a bunch of /operators/ : =WithFile=, =WithDir=, … 76 77 #+BEGIN_SRC go 78 func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir { 79 // … 80 } 81 82 func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File { 83 // … 84 } 85 #+END_SRC 86 87 The =With*= function are all satisfying the =PathOp= interface, making =NewFile= and 88 =NewDir= extremely composable. Let's first see how our above example would look like using 89 the =fs= package, and then, we'll look a bit more at the main =PathOp= function… 90 91 #+BEGIN_SRC go 92 dir := fs.NewDir(t, "bar", fs.WithDir("foo", 93 fs.WithFile("bar", fs.WithContent("content"), fs.WithMode(os.FileMode(0777))), 94 )) 95 defer dir.Remove() 96 #+END_SRC 97 98 It's clean and simple to read. The intent is well described and there is not that much of 99 noise. =fs= functions tends to have /sane/ and /safe/ defaults value (for =os.FileMode= 100 for example). Let's list the main, useful, =PathOp= provided by =gotest.tools/fs=. 101 102 - =WithDir= creates a sub-directory in the directory at path. 103 - =WithFile= creates a file in the directory at path with content. 104 - =WithSymlink= creates a symlink in the directory which links to target. Target must be a 105 path relative to the directory. 106 - =WithHardlink= creates a link in the directory which links to target. Target must be a 107 path relative to the directory. 108 - =WithContent= and =WWithBytes= write content to a file at Path (from a =string= or a 109 =[]byte= slice). 110 - =WithMode= sets the file mode on the directory or file at path. 111 - =WithTimestamps= sets the access and modification times of the file system object at 112 path. 113 - =FromDir= copies the directory tree from the source path into the new Dir. This is 114 pretty useful when you have a huge folder structure already present in you =testdata= 115 folder or elsewhere. 116 - =AsUser= changes ownership of the file system object at Path. 117 118 Also, note that =PathOp= being an function type, you can provide your own implementation 119 for specific use-cases. Your function just has to satisfy =PathOp= signature. 120 121 #+BEGIN_SRC go 122 type PathOp func(path Path) error 123 #+END_SRC 124 125 * Compare folder structures 126 127 Sometimes, the code you're testing is creating a folder structure, and you would like to 128 be able to tests that, with the given arguments, it creates the specified structure. =fs= 129 allows you to do that too. 130 131 The package provides a =Equal= function, which returns a =Comparison=, that the [[/posts/2018-08-16-gotest-tools-assertions/][=assert=]] 132 package understand. It works by comparing a =Manifest= type provided by the test and a 133 =Manifest= representation of the specified folder. 134 135 #+BEGIN_QUOTE 136 Equal compares a directory to the expected structured described by a manifest and returns 137 success if they match. If they do not match the failure message will contain all the 138 differences between the directory structure and the expected structure defined by the 139 Manifest. 140 #+END_QUOTE 141 142 A =Manifest= stores the expected structure and properties of files and directories in a 143 file-system. You can create a =Manifest= using either the functions =Expected= or 144 =ManifestFromDir=. 145 146 We're going to focus on the =Expected= function, as =ManifestFromDir= does pretty much 147 what you would expected : it takes the specified path, and returns a =Manifest= that 148 represent this folder. 149 150 #+BEGIN_SRC go 151 func Expected(t assert.TestingT, ops ...PathOp) Manifest 152 #+END_SRC 153 154 =Expected= is close to =NewDir= function : it takes the same =PathOp= functional 155 arguments. This makes creating a =Manifest= straightforward, as it's working the same. Any 156 function that satisfy =PathOp= can be used for =Manifest= the exact same way you're using 157 them on =fs.NewDir=. 158 159 There is a few additional functions that are only useful with =Manifest= : 160 161 - =MatchAnyFileContent= updates a Manifest so that the file at path may contain any content. 162 - =MatchAnyFileMode= updates a Manifest so that the resource at path will match any file mode. 163 - =MatchContentIgnoreCarriageReturn= ignores cariage return discrepancies. 164 - =MatchExtraFiles= updates a Manifest to allow a directory to contain unspecified files. 165 166 #+BEGIN_SRC go 167 path := operationWhichCreatesFiles() 168 expected := fs.Expected(t, 169 fs.WithFile("one", "", 170 fs.WithBytes(golden.Get(t, "one.golden")), 171 fs.WithMode(0600)), 172 fs.WithDir("data", 173 fs.WithFile("config", "", fs.MatchAnyFileContent)), 174 ) 175 176 assert.Assert(t, fs.Equal(path, expected)) 177 #+END_SRC 178 179 The following example compares the result of =operationWhichCreatesFiles= to the expected 180 =Manifest=. As you can see it also integrates well with other part of the =gotest.tools= 181 library, with the [[/posts/2018-09-06-gotest-tools-golden/][=golden= package]] in this example. 182 183 * Conclusion… 184 185 … that's a wrap. In my opinion, this is one the most useful package provided by 186 =gotest.tools= after =assert=. It allows to create simple or complex folder structure 187 without the noise that usually comes with it.