I ran into what seemed like very strange behavior from Dart today. There must be something wrong with how I am understanding local variables and lexical scope. Let's get into some code. The context is a testing grade conversion from numbers to letters, but that's not crucial to understand.
First, consider this code, which works as expected:
void main() {
final pairs = <List>[
[95, 'A'],
[85, 'B'],
];
for (final pair in pairs) {
int number = pair[0];
String letter = pair[1];
test('$number converts to $letter', () {
final actual = convert(number);
expect(actual, letter);
});
}
}
String convert(int value) => value >= 90 ? 'A' : 'B';
Run the tests and it passes. Change the second row of test data from 85 to 95, and the second test fails. I understand this.
Now consider this alternative implementation. Notice that the only difference is that the number
and letter
variables are now declared outside the for
loop.
void main() {
final pairs = <List>[
[95, 'A'],
[85, 'B'],
];
int number;
String letter;
for (final pair in pairs) {
number = pair[0];
letter = pair[1];
test('$number converts to $letter', () {
final actual = convert(number);
expect(actual, letter);
});
}
}
String convert(int value) => value >= 90 ? 'A' : 'B';
Running this code as-is produces the expected results: two tests succeed, and their names are "95 converts to A" and "85 converts to B". However, now change that 85 in the test data to 95. Now, both tests fail. The names of the tests are what one would expect: "95 converts to A" and "95 converts to B."
I've tried setting a breakpoint inside the test method to figure it out. When the test is run, the first test's name is "95 converts to A," but inside that test body, the value of letter
is 'B'.
I've been dealing with programming languages for a long time, but I'm dumbfounded. Can someone explain to me what is happening here? I don't understand why the two programs would behave differently.
The way the test framework works, is that it runs once to find all the tests, but doesn't actually execute the test bodies yet.
Then, the test runner runs the tests that it collected in the first pass.
Your code creates one set of variables, and assigns to them twice in the first pass, and registers two tests. Then the tests run, and both see the latest assignment to the variables.
That's why you need to either ensure that the variables are unique to the call to test
, by creating them inside the loop, or ensure the variables are initialized by a setUp
call, because those are called for each test.