home

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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.