There were two problems here.
First, shouldReceive('bar') mocks the bar function (and without ->andReturns(...), it will do nothing and return null), so I should use shouldHaveReceived('bar') at the end of my test instead.
But also, spy(Bar::class) creates mocks for all the functions in Bar (including bar), unless I explicitly create a partial mock, leading to this test that does exactly what I want:
public function test_foobar()
{
$spy = $this->spy(Bar::class)->makePartial();
$foo = $this->app->get(Foo::class);
$this->assertEquals(42, $foo->foo());
$spy->shouldHaveReceived('bar');
}