Tuesday, November 03, 2015

Test Package Naming in Go

I’m still – still – playing with Google’s Go language, making the occasional one-off utility and also working on a Crazy Web Server Project. I hesitate to call it a Framework, since all Frameworks are ultimately doomed. It’s nothing too extreme, but it’s (hopefully) going to solve some of my web-server problems for small sites; and it’s (absolutely) helping me learn Go “for reals.”

I recently discovered something very simple, more or less by accident, that has big implications. It is this: tests for package foo should declare package foo_test.

To me this was counterintuitive, since one of the big hairy things you have to adjust to when learning go is the flat package directory. A package is made up of a bunch of .go files, and they all live in one flat directory. If you want to organize the files in a hierarchical set of directories, you must also have a corresponding set of packages (though they need not be hierarchical).

Incidentally, as I get better at Go I find myself hating such conventions less, because they are useful in forcing the programmer’s hand: be idiomatic damn you! And that certainly has its upsides.

Thus, logically, every .go file in a given directory declares the same package. If it doesn’t, you get a compile-time error!

Except, that is, for tests.

The Test Exception

For a directory with a package foo, you may have two packages: foo and foo_test. You may not have any others. So, why would you want to use foo_test?

There is a very good reason, which I’ll get to in a moment. If you already have a lot of unit-testing experience and know a little Go you may already have guessed.

Let’s first consider the way I had been doing it until recently. Apologies in advance for the lack of syntax highlighting.

foo.go

package foo

func Bar() string { return "BAZ"}

foo_test.go


package foo

import "testing"

func Test_Bar(t *testing.T) {

    exp := "BAZ"
    got := Bar()
    if got != exp {
        t.Errorf("Expected '%s', got '%s'", exp, got)
    }
}

OK, great. This makes perfect sense so far. But now consider this, which in fact seems quite the obvious thing to do:

foo.go

package foo

func Bar() string { return ook(1) }

func ook(i int) string {
    if i > 0 {
        return "BAZ"
    } else {
        return "BAT"
    }
}

foo_test.go

package foo

import "testing"

func Test_Bar(t *testing.T) {

    exp := "BAZ"
    got := Bar()
    if got != exp {
        t.Errorf("Expected '%s', got '%s'", exp, got)
    }
}

func Test_ook(t *testing.T) {

    exp0 := "BAT"
    got0 := ook(0)
    if got0 != exp0 {
        t.Errorf("For 0, expected '%s', got '%s'", exp0, got0)
    }

    exp1 := "BAZ"
    got1 := ook(1)
    if got1 != exp1 {
        t.Errorf("For 1, expected '%s', got '%s'", exp1, got1)
    }

}

The example above shows a very common pattern, about whose virtue we could certainly argue: you can have much more thorough test coverage if you have robust unit tests for private functions. (Remember, in Go only Capitalized functions are exported.)

However, there’s a cost in clarity: you should be testing all things that could happen in the use of the package, i.e. you should be testing all variations of the public API. If there are conditions your public API can not trigger, and which thus can not be tested via the public API, then remove them from the code.

Go is strongly biased towards writing only the code that is actually used. Speculative code, while possible, is definitely not idiomatic.

As somebody who writes code for a lot of “what-if” scenarios in Perl, I can certainly apprecitate that discipline.

This is why you should use the foo_test package: Go will then throw a compile time error if you try to test a private function, and in your quest for 100% code coverage you will see the unreachable parts of your code.

So let’s get back to our example, changing the test first:

foo_test.go

package foo_test

import (
    "./"
    "testing"
)

func Test_Bar(t *testing.T) {

    exp := "BAZ"
    got := foo.Bar()
    if got != exp {
        t.Errorf("Expected '%s', got '%s'", exp, got)
    }
}

If Bar is the only thing that ever calls ook then you’ll have to change ook or you’ll never get 100% coverage. In this contrived example, the compiler will most likely abstract away ook entirely, but bear with me.

foo.go

package foo

func Bar() string { return ook() }

func ook() string {
    return "BAZ"
}

And there we have it, in silly little examples. Always use a _test package name for your Go tests. Testing will be harder. Deal with it: your tests will also be better.

Labels: , , , ,

Sunday, November 01, 2015

Never Pay Marhaba In Advance.

I recently flew to lovely, hazy Singapore, with a stopover in Dubai. It was a trip of many firsts: my first time in Asia, let alone Singapore; my first time flying Emirates; my first flight on an Airbus A380; and my first time in the famous Dubai Airport.

Since I had arranged for a four-hour stopover, I thought it would be nice to kill the time in a lounge. Since my lovely wife and I were traveling coach – not half bad on the A380 – the business lounges were out of the question. But fortunately there was a pay lounge avaialable: Marhaba.

After first getting a bite to eat elsewhere, we went to the Marhaba desk and asked about buying in. The attendants were quite friendly, and told us the lounge was rather full and thus we might not have a seat. I looked around, and while it was indeed full, there were some bistro-style chairs at a table. Good enough. We paid about 100 EUR for the two of us, and enjoyed the lounge.

I was planning on recommending it: it’s not as nice as a real business lounge, but it’s not too expensive and it beats just killing time in the airport. Plus, they mix a strong G & T.

However, the trip home went rather differently, and hence instead of a glowing recommendation I give you this advice:

Never

Pay

Marhaba

In

Advance

Why not, you ask? Wouldn’t it be cheaper that way, you ask?

Well yes, it would be cheaper, except for the part about Marhaba keeping your money if your flight is delayed.

Our flight from Singapore to Dubai was significantly delayed after boarding. We sat on the plane for over two hours waiting for the engines to be replaced, or whatever technical defect they needed to fix (the Captain left us guessing). Then we were further delayed in the air; and just to make sure everyone was in the best possible mood, Emirates canceled the dinner service, at least in Coach.

When we arrived in Dubai, with clearly insufficient time to make our connection, we hustled through security and asked an attendant what we should do. He told us we should hurry. And so we jogged to the gate, and found the plane waiting.

About ten minutes later we were boarded, along with a few slower joggers, and on our way back to Europe.

Marhaba Will Not Refund Your Money.

I had booked the Marhaba lounge, pre-paid at a small discount, back in Singapore this time, as I figured it would be better that way. I had entered my flight number on their web site, so their system was fully aware of the delay.

Once back at a computer, I sent them an e-mail politely reminding them that my flight had been so delayed it would have been physically impossible to go to the lounge before making my connection, and asked them to refund my payment if they had not done so yet. (Honestly I half expected them to have issued a refund already, since they knew about the flight, but then I am a computer programmer.)

Marhaba got back to me promptly and told me that unfortunately, since I failed to cancel the booking eight hours in advance – i.e., since I failed to call them from the air – they would be keeping my money. Have a nice day.

I thought, well, that can’t really be your policy if you’re providing airport services, can it? This must be a pretty common occurrence: flight delayed, services can’t be used, at least issue a credit for the next time, right?

Bear in mind that lounge access is only one of the services Marhaba provides. You can easily shell out hundreds of Euros (thousands of Dirham) on Marhaba services, and then what happens when your flight is delayed, or for that matter canceled?

So I asked for clarification. And I got it:

We regret to inform you that no refund will be processed for a no show booking since any amendments or cancellation should be done at least 10hrs prior to the flight. Please be advised that all no show bookings has no refund.

Never Pay Marhaba In Advance.

Maybe that’s just how they roll in the Emirates. Maybe it’s usually somebody else’s money. Maybe everyone’s rich enough they don’t care about such things.

But if you care about your money, or about the principle of the matter, then it’s imperative that you never pay Marhaba in advance, for anything.

If I fly through Dubai again I might use their walk-in lounge service, since a strong drink is a strong drink, but my credit card shall never again darken the Marahaba website’s door. Neither should yours.

Labels: , , ,