The main reason you should not call execute directly is because it does not reset your governor limits, which may give you bad test results. For example, you can't call as many Queueable classes in asynchronous code as you can in synchronous code, so your test will pass, but fail in production. Alternatively, your code might exceed the synchronous heap limit, CPU limit, etc, artifically failing during deployment even though your code should work in practice.
There are times when it is okay to use the latter, but it should be avoided whenever possible. Please note that "best practices" doesn't mean "this is a law that must not be violated," but rather "this is a good thumb of rule you should observe as much as practical." In many cases, it doesn't matter, and in some cases, you very explicitly need to call the execute method directly because it violates a unit test limit that doesn't exist in production mode.
For example, if you have a chaining Batchable class, you might need to call the execute methods directly so you can test them without triggering the chaining batch jobs, which would violate unit test rules, but would work okay in normal execution. So, as you can see, there are times where calling execute directly must be done, but it should be considered a last resort in order to make sure you're getting accurate test results.