/leagueendpoint should return the players ordered by the number of wins!
PlayerStoreabstraction we have used.
InMemoryPlayerStorefor now so that the integration tests keep passing as we develop our new store. Once we are confident our new implementation is sufficient to make the integration test pass we will swap it in and then delete
io.Reader), writing data (
io.Writer) and how we can use the standard library to test these functions without having to use real files.
PlayerStoreso we'll write tests for our store calling the methods we need to implement. We'll start with
strings.NewReaderwhich will return us a
Reader, which is what our
FileSystemPlayerStorewill use to read data. In
mainwe will open a file, which is also a
FileSystemPlayerStorein a new file
Readerbut not expecting one and it doesn't have
league.goand put this inside.
Reada second time?
Readerhas reached the end so there is nothing more to read. We need a way to tell it to go back to the start.
FileSystemPlayerStoreto take this interface instead?
string.NewReaderthat we used in our test also implements
ReadSeekerso we didn't have to make any other changes.
Writerbut we already have our
ReadSeeker. Potentially we could have two dependencies but the standard library already has an interface for us
ReadWriteSeekerwhich lets us do all the things we'll need to do with a file.
strings.Readerdoes not implement
ReadWriteSeekerso what do we do?
ReadWriteSeeker. The pro of this is it becomes more of an integration test, we're really reading and writing from the file system so it will give us a very high level of confidence. The cons are we prefer unit tests because they are faster and generally simpler. We will also need to do more work around creating temporary files and then making sure they're removed after the test.
ReadWriteSeeker(the file) but also a function. We need to make sure that the file is removed once the test is finished. We don't want to leak details of the files into the test as it's prone to error and uninteresting for the reader. By returning a
removeFilefunction, we can take care of the details in our helper and all the caller has to do is run
./file_system_store_test.go:67:8: store.RecordWin undefined (type FileSystemPlayerStore has no field or method RecordWin)
rangeover a slice you are returned the current index of the loop (in our case
i) and a copy of the element at that index. Changing the
Winsvalue of a copy won't have any effect on the
leagueslice that we iterate on. For that reason, we need to get the reference to the actual value by doing
league[i]and then changing that value instead.
RecordWin, we are iterating over
Playerto find a player by name.
FileSystemStorebut to me, it feels like this is maybe useful code we can lift into a new type. Working with a "League" so far has always been with
Playerbut we can create a new type called
League. This will be easier for other developers to understand and then we can attach useful methods onto that type for us to use.
league.goadd the following
Leaguethey can easily find a given player.
PlayerStoreinterface to return
Player. Try to re-run the tests, you'll get a compilation problem because we've changed the interface but it's very easy to fix; just change the return type from
Leaguecan be refactored.
nilbecause it couldn't find the player.
Storein the integration test. This will give us more confidence that the software works and then we can delete the redundant
TestRecordingWinsAndRetrievingThemreplace the old store.
main.gowill now have compilation problems which will motivate us to now use our new store in the "real" code.
os.OpenFilelets you define the permissions for opening the file, in our case
O_RDWRmeans we want to read and write and
os.O_CREATEmeans create the file if it doesn't exist.
GetPlayerScore()we are reading the entire file and parsing it into JSON. We should not have to do that because
FileSystemStoreis entirely responsible for the state of the league; it should only need to read the file when the program starts up and only need to update the file when data changes.
FileSystemStoreto be used on the reads instead.
FileSystemPlayerStoreso just fix them by calling our new constructor.
Seekback to the start of the file and then write the new data—but what if the new data was smaller than what was there before?
Tape. Create a new file with the following:
Writenow, as it encapsulates the
Seekpart. This means our
FileSystemStorecan just have a reference to a
RecordWin. Yes, it doesn't feel much, but at least it means if we do any other kind of writes we can rely on our
Writeto behave how we need it to. Plus it will now let us test the potentially problematic code separately and fix it.
tape, and read it all again to see what's in the file. In
os.Filehas a truncate function that will let us effectively empty the file. We should be able to just call this to get what we want.
tapeto the following:
io.ReadWriteSeekerbut we are sending in
*os.File. You should be able to fix these problems yourself by now but if you get stuck just check the source code.
TestTape_Writetest should be passing!
RecordWinwe have the line
Encoderin our type and initialise it in the constructor:
io.Readeras that was the easiest path for us to unit test our new
PlayerStore. As we developed the code we moved on to
io.ReadWriteSeeker. We then found out there was nothing in the standard library that actually implemented that apart from
*os.File. We could've taken the decision to write our own or use an open source one but it felt pragmatic just to make temporary files for the tests.
Truncatewhich is also on
*os.File. It would've been an option to create our own interface capturing these requirements.
*os.Fileso we don't need the polymorphism that interfaces give us.
league, _ := NewLeague(f.database)in our constructor.
NewLeaguecan return an error if it is unable to parse the league from the
io.Readerthat we provide.