Ideally, you should run a separate machine for each environment (production, uat, test, dev and ci). If you do not have resources for real physical machines, then virtualization is the way to go for non-production environments.
It also means that you can correctly test the effects of the various dependencies and libraries that you use.
EDIT: About branching ...
What we do here, and in several places that I worked with before, has an integration branch from the trunk. Developers developing new features move away from the integration industry and reintegrate into the integration industry. CI is performed both by integration and by trunk. Unofficial testing can be done during integration, but more formal testing (UAT releases) comes from the trunk. Periodically, we integrate down from the integration branch into the trunk. This has the added benefit of protecting the barrel.
i.e.
trunk integration feature1 feature2
source share