At the end of Part 1 I had just completed my first test for parsing XML from the European Central Bank (ECB) currency rate service.

Reading XML

	f, err := os.Open("testdata/eurofxref-daily.xml")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	rates, err := parse(f)

That call says that I want my parse function to take an open file.

The documentation for Open says that it returns a *File object.

The documentation for the XML Decoder says that it requires an io.Reader, so, I go and check what io.Reader` needs, and it’s fairly clear:

type Reader interface {
    Read(p []byte) (n int, err error)
}

By comparing this with the declaration of File.Read I can see that it clearly implements the same method.

Go’s interfaces are a formalisation of duck-typing, our File provides a way to read into a slice buffer, and that’s all the XML decoder needs.

Implementing the parser was a bit trial and error, and that initial test was very helpful in getting to the result.

The Parser

func parse(f io.Reader) ([]*EuroRate, error) {
	decoder := xml.NewDecoder(f)
	rates := make([]*EuroRate, 0)
	var currentDate string
	for {
		token, err := decoder.Token()
		if err != nil {
			return nil, err
		}

		if err == io.EOF || token == nil {
			break
		}

		switch se := token.(type) {
		case xml.StartElement:
			if se.Name.Local == "Cube" {
				date := getDateFromElement(se)
				if date != "" {
					currentDate = date
				} else {
					rate := getEuroRateFromElement(se, currentDate)
					if rate != nil {
						if currentDate == "" {
							return nil, errors.New("invalid XML: found rate without current date")
						}
						rates = append(rates, rate)
					}
				}
			}
		}
	}

	return rates, nil
}

It starts by wrapping the provided io.Reader in an XML decoder.

Then creates a new slice of pointers to EuroRate structs, with size 0.

The parser doesn’t know how many items it will find up front, and the slice will dynamically grow, there could be some performance optimisation here to put in a default number of currencies as the size, and let it grow if I get more, but for this initial implementation, this isn’t important.

There’s a fair bit of error handling when decoding tokens from the stream, if I get an error, I return the error up the call stack.

This code all uses the stdlib, but in production, I’d recommend the errors package, which can wrap errors making it easier to get list of the functions that an error passed through, a common mistake in Go is to just return the error that you get from an external call, and all that bubbles up to the top is the message, something like could not open File, without any easy way to identify which file, or where it couldn’t be opened.

After that, I use a type switch to identify StartElement tokens.

The rest of the function ensures that I’m parsing the XML correctly, and instantiates EuroRate objects from the data in the XML token, and then appends it to the slice that is eventually returned.

// tries to extract a EuroRate from the provided element
// the element may not be a valid rate element (they're all called Cube) and in
// this case, returns nil.
func getEuroRateFromElement(se xml.StartElement, date string) *EuroRate {
	var code string
	var rate string
	for _, attr := range se.Attr {
		switch attr.Name.Local {
		case "currency":
			code = attr.Value
		case "rate":
			rate = attr.Value
		}
	}
	if code != "" && rate != "" && date != "" {
		return &EuroRate{Currency: code, Rate: rate, ReferenceDate: date}
	}
	return nil
}

Tries to extract a EuroRate from this XML.

  <Cube currency='USD' rate='1.1243'/>

This code parses the attributes from the XML element , and returns valid EuroRate values from the items.

If it can’t find the relevant attributes, this might not be a rate element, so I can skip this this one.

At this point, I have a simple function that can take an object that can be read from, and parse XML from it.

Further tests to validate the parsing

func TestParseDailyRatesCurrencies(t *testing.T) {
	f, err := os.Open("testdata/eurofxref-daily.xml")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	rates, err := parse(f)

	ratesToCheck := []struct {
		element  int
		currency string
		rate     string
		date     string
	}{
		{0, "USD", "1.1243", "2017-05-21"},
	}

	for _, tt := range ratesToCheck {
		rate := rates[tt.element]
		if rate.Currency != tt.currency {
			t.Errorf("unexpected currency parsed (%d): got %s, wanted %s", tt.element, rate.Currency, tt.currency)
		}

		if rate.Rate != tt.rate {
			t.Errorf("unexpected rate parsed (%d): got %s, wanted %s", tt.element, rate.Rate, tt.rate)
		}

		if rate.ReferenceDate != tt.date {
			t.Errorf("unexpected date parsed (%d): got %s, wanted %s", tt.element, rate.ReferenceDate, tt.date)
		}
	}
}

This uses a very idiomatic Go testing method, Table Driven Tests.

The ratesToCheck := []struct{ ... } declaration creates an anonymous struct, with four members, element, currency, rate and date.

It then provides struct literals, {0, "USD", "1.1243", "2017-05-22"} to populate an array literal (the []struct) declaration.

The actual tests, iterate over the table, asserting the rate I got, matched what I expected.

Table tests are really useful, they make it easy to extend your tests and add additional cases, and can often be shared amongst several tests, by adding extra columns.

Junit 5 has a version of these, but in my experience, they’re close, but not quite as nice.

Some other things to note from these tests:

  • These use Errorf instead of Fatalf, the Fatal functions in the testing packge are assertions, rather than checks.

    This means that all of the tests in the table are guaranteed to run, rather than stopping after the first one.

  • They include the element number in the output of the test, so that you can work out which ones went wrong.

  • Finally, they use a very terse output message, “got %s, wanted %s”, this is fairly common in Go, see here and here for examples from the standard library.