Post
s, and then a separate NewHandler
function will use those Post
s as a datasource for the blog's webserver.Post
s.BlogPostFileParser
.Optimism is an occupational hazard of programming. Feedback is the treatment.
Package fs defines basic interfaces to a file system. A file system can be provided by the host operating system but also by other packages.
io.fs
, io.Reader
, io.Writer
), is vital to writing loosely coupled packages. These packages can then be re-used in contexts different to those you imagined, with minimal fuss from your consumers.[]Post
returned is the same as the number of files in our fake file system.mkdir blogposts
cd blogposts
go mod init github.com/{your-name}/blogposts
touch blogposts_test.go
blogposts_test
. Remember, when TDD is practiced well we take a consumer-driven approach: we don't want to test internal details because consumers don't care about them. By appending _test
to our intended package name, we only access exported members from our package - just like a real user of our package.testing/fstest
which gives us access to the fstest.MapFS
type. Our fake file system will pass fstest.MapFS
to our package.A MapFS is a simple in-memory file system for use in tests, represented as a map from path names (arguments to Open) to information about the files or directories they represent.
blogposts.go
and put package blogposts
inside it. You'll need to then import that package into your tests. For me, the imports now look like:NewPostsFromFS
function, that returns some kind of collection.Sliming is useful for giving a “skeleton” to your object. Designing an interface and executing logic are two concerns, and sliming tests strategically lets you focus on one at a time.
Post
for each one and, return the slice.fstest.MapFS
. But, it doesn't have to be. Change the argument to our NewPostsFromFS
function to accept the interface from the standard library.error
.fs.ReadDir
. To do this "properly", we'd need a new test where we inject a failing fs.FS
test-double to make fs.ReadDir
return an error
.Post
type so that it has some useful data.Post
type so that the test will runfs.FS
gives us a way of opening a file within it by name with its Open
method. From there we read the data from the file and, for now, we do not need any sophisticated parsing, just cutting out the Title:
text by slicing the string.DoesnewPost
have to be coupled to anfs.File
? Do we use all the methods and data from this type? What do we really need?
io.ReadAll
which needs an io.Reader
. So we should loosen the coupling in our function and ask for an io.Reader
.getPost
function, which takes an fs.DirEntry
argument but simply calls Name()
to get the file name. We don't need all that; let's decouple from that type and pass the file name through as a string. Here's the fully refactored code:newPost
. The concerns of opening and iterating over files are done, and now we can focus on extracting the data for our Post
type. Whilst not technically necessary, files are a nice way to logically group related things together, so I moved the Post
type and newPost
into a new post.go
file.Posts
a lot, so we should write some code to help with thatPost
.bufio.Scanner
Scanner provides a convenient interface for reading data such as a file of newline-delimited lines of text.
io.Reader
to read through (thank you again, loose-coupling), we don't need to change our function arguments.Scan
to read a line, and then extract the data using Text
.error
. It would be tempting at this point to remove it from the return type, but we know we'll have to handle invalid file structures later so, we may as well leave it.strings.TrimPrefix
.readMetaLine
to get the next line for the tags and then split them up using strings.Split
.Body
to Post
and the test should fail.---
separator.scanner.Scan()
returns a bool
which indicates whether there's more data to scan, so we can use that with a for
loop to keep reading through the data until the end.Scan()
we write the data into the buffer using fmt.Fprintln
. We use the version that adds a newline because the scanner removes the newlines from each line, but we need to maintain them.newPost
, without having to concern themselves with implementation specifics..md
fs.FS
, and the other changes in Go 1.16 give us some elegant ways of reading data from file systems and testing them simply.cmd
folder within the project, add a main.go
fileposts
folder and run the program!io.Writer
to keep your code loosely-coupled and re-usable.io/fs
. Ben Congdon has done an excellent write-up which was a lot of help for writing this chapter.