Writing functional tests for a controller that makes call to external service? I'm working on the D8 version of the CAS module. To integrate with a single sign on CAS server, this module will have to redirect the user to the CAS server often, and occasionally make cURL requests to that server for validation of various tokens. I'm struggling to write functional tests because of this.

I have a Drupal controller that will redirect the user to a number of places and invoke actions on a number of services, some of which may make cURL requests to the CAS server.

When the controller redirects to the server, I can just tell my web test to NOT follow redirects by overriding the property maxiumRedirects and setting it 0, then inspecting the response to make sure it was a redirect.

But, Since this controller may invoke services that make cURL requests to the CAS server, how would I handle that in a functional test, where that external service does not exist? In a unit test, I can mock the response from Guzzle, but can I do that in a functional test? Is my only option to create a test "server" module (with all the routes defined that CAS client makes requests to) that is enabled only for testing and would be used for testing my CAS client module?
Depending on what exactly you want to do, what we've done multiple times is make the domain/URL to which you redirect to configurable, and then configure it to redirect back to your site.

Then implement dummy routes there in a test module, do whatever you want and redirect back.

This was already possible in 7.x and hasn't really changed. It is sometimes more complicated to set up, but has the advantage that you are testing much more of your actual code and integration.

if you use @larowlan's approach, you could also just write unit tests for your service but I like to have most code covered with functional tests (too). I've seen it in core already many times that unit tests and their mocks get out of sync with how things really work, and then they are testing something that no longer exists. The more you have to mock, the bigger that risk becomes.

Note that, no matter which approach you chose, moving as much logic as possible into a service or even multiple services makes sense anyway and allows you to use unit tests for things that they are good at. Testing all possible cases, error handling/conditions and so on.