5

This question is about starting a rails server of the external project from a rspec environment.

There is 2 projects.

  • First project act as the Admin Back Office, it's the central application where users interact with web pages. I call it BackOffice
  • Second project is a Json API Server which will receive commands from the Admin Back Office through json requests.I call it ApiServer

I am trying to test API interaction between those 2 rails projects, and I would like to set-up rspec so I can write and maintain my spec files in BackOffice project. Those specs would start a ApiServer rails server and then play around to perform the tests.

My issue is about starting the ApiServer rails server. After looking at the rails app initialization files, I assumed I had to add a require to "config/environment".


But when I insert into BackOffice/spec/spec_helper.rb

require File.expand_path('../../../ApiServer/config/environment', __FILE__)

I get the error

`initialize!': Application has been already initialized. (RuntimeError)
# Backtrace to the file:
# ApiServer/config/environment.rb
# Line:
# Rails.application.initialize!

I also tried to simply call the following in backticks

`cd /api/path; bundle exec rails s -p 3002` 

but got the same kind of error


Then I got inspiration from Capybara source code, and required the "ApiServer/application", then I am able to create a ApiServer.new object, but as soon as I call initialize! on it it I get the same message.

Any help is greatly appreciated. Cheers

Benjamin Bouchet
  • 12,971
  • 2
  • 41
  • 73
  • https://github.com/rails/rails/blob/3e36db4406beea32772b1db1e9a16cc1e8aea14c/railties/lib/rails/application.rb#L58 As you can see here, you can only initialize one rails without overriding the initializer in the `Rails::Application` – Yury Lebedev Sep 04 '15 at 10:01
  • so if you really think you want to do this, you can override the `initialize` method of the `Rails::Application` before requiring the `environment` file of the second app – Yury Lebedev Sep 04 '15 at 10:02
  • Thanks Yury. I just had a look at your suggestion and cannot figure it out yet, will keep looking in this direction. Or perhaps Something can be done in Capybara. – Benjamin Bouchet Sep 05 '15 at 06:58
  • Are you sure you don't actually have an instance of the APIServer already running? What is the exact error you get from the back tick version? – Brad Sep 11 '15 at 19:16
  • Have you considered combining the projects with a [Rails Engine](http://guides.rubyonrails.org/engines.html)? Task rabbit has a [great blog](http://tech.taskrabbit.com/blog/2014/02/11/rails-4-engines/) on how to enginize a rails application as a reference. – Pyrce Sep 13 '15 at 05:10
  • I haven't. It's an idea though – Benjamin Bouchet Sep 13 '15 at 05:13

4 Answers4

1

Actually the second app is nothing more then an external service, which is better to stub for the tests.

There is one nice article from thoughtbot about using vcr gem to mock external web services:

https://robots.thoughtbot.com/how-to-stub-external-services-in-tests

Yury Lebedev
  • 3,985
  • 19
  • 26
  • Thanks for you answer. I already covered both ends of the API with stubbed tests. I am not asking if I should, I am asking how to do it. – Benjamin Bouchet Sep 04 '15 at 09:21
  • I actually suggest you to avoid making real requests to the second app in your tests, and stub the response from the second app for your interaction tests. Because otherwise you also need to create some test data in you api app, and you will need to do it from tests inside your `backoffice` app. – Yury Lebedev Sep 04 '15 at 09:39
  • I think a good practice would be to write a api client gem, that would have it's own tests, and a dummy server (sinatra for example) for the tests, which you can include in your gemfile of the `backoffice` app, and start the dummy server there in the `test_helper`. – Yury Lebedev Sep 04 '15 at 09:40
  • @BenjaminSinclaire if you really insist on having integration tests against real api (which has it's upsides of course) then you should probably do it against staging environment. – Mike Szyndel Sep 06 '15 at 21:32
  • @MichalSzyndel yes I'm ok with this. Still I don't know how to start the API server from my rspec. – Benjamin Bouchet Sep 07 '15 at 03:30
  • After some looking around here you can find a solution, though still discouraged http://stackoverflow.com/questions/26185467/start-another-rails-server-from-within-rails-app-with-backticks – Mike Szyndel Sep 07 '15 at 10:48
1

Obligatory "don't do that unless you really need to".

However, since it seems you know what you need:

Short answer:

You need to isolate both application in system environment and launch it from there using system-calls syntax.

Long answer:

What you're trying to do is to run two Rails applications in the same environment. Since they both are Rails applications they share a lot of common names. Running them ends in name clash, which you're experiencing. Your hunch to try simple back ticks was good one, unfortunately you went with a bundler in already existing environment, which also clashes.

What you have to do in order to make it work is to properly isolate (in terms of code, not in terms of network i.e. communication layer ) application and then run launcher from rspec. There are multiple ways, you could:

  • Use Ruby process control (Check this graph, you could try to combine it with system level exec)
  • Daemonize from Operating System level (init.d etc.)
  • Encapsulate in VM or one of the wrappers (Virtualbox, Vagrant, etc.)
  • Go crazy and put code on separate machine and control it remotely (Puppet, Ansible, etc.)

Once there, you can simply run launcher (e.g. daemon init script or spawn new process in isolated environment) from RSpec and that's it.

Choosing which way to go with is highly dependent on your environment. Do you run OSX, Linux, Windows? Are you using Docker? Do you manage Ruby libraries through things like RVM? Things like this.

Community
  • 1
  • 1
XLII
  • 1,172
  • 9
  • 18
  • Thanks for your answer, I tried this direction but I'm not experienced in this domain and did not success to completely isolate the environments (tried using `fork` without success), perhaps I need a bit more help to find exactly how to do it. I'm using linux and rbenv. I'd rather stay local if possible, so I won't have to deploy the ApiServer for every bit of test. – Benjamin Bouchet Sep 07 '15 at 10:53
  • Personally for complex environments or the ones I don't like setting up (like installing global PHP/Apache into workstation) I usually go with [Vagrant](https://www.vagrantup.com/). In my opinion it's easy to setup and works really well. – XLII Sep 07 '15 at 11:13
  • Vagrant is not free. its a no go for me. I'd rather go for a solution based on ruby process control. Following the graph you linked I'd go for IO.popen. Would you min to write code example for me? – Benjamin Bouchet Sep 08 '15 at 00:45
  • Vagrant is [open source](https://github.com/mitchellh/vagrant) and available for free with VirtualBox, however they offer paid VMWare integration, which you're most likely referring to. – XLII Sep 08 '15 at 06:51
  • Still, I don't like the vagrant solution. Too heavy. – Benjamin Bouchet Sep 09 '15 at 08:02
  • Can you provide any additional help on the `popen` solution? – Benjamin Bouchet Sep 12 '15 at 05:21
  • Unfortunately as far as this goes, this is highly dependent on your operating system environment, so no solution is guaranteed to run everywhere. On the surface `popen("/bin/bash -l -c PATH/CMD")` should work on *NIX, however if it will work depends on your OS/rbenv etc configuration and few other factors. This is something you should consult with your sysadmin if it doesn't work, or maybe on [Server Fault](http://serverfault.com/). I'm not including this into answer since it's too volatile and system-specific. – XLII Sep 12 '15 at 10:26
  • Ok so I tried IO.popen, but it's still not working: the ApiServer's Gemfile is never loaded, and the BackOffice's Gemfile is used during initialisation of the ApiServer within the IO.popen block – Benjamin Bouchet Sep 20 '15 at 07:08
  • That's what makes most difficult part in this solution. You need to ensure that the proper environment is loaded, but also you need to ensure, that it isolates properly. IO.popen for instance, might isolate process for me, but not for you due to your system specific configuration. You might want to try `popen("/bin/bash -l -c bundle exec PATH/CMD")`/rbenv specific wrapping or various others, but from outside it's guesswork. Ruby path is most volatile and difficult of all due to this. I suggest going with daemonizing or VM wrapping which is much easier. – XLII Sep 20 '15 at 08:26
1

Generally it's a bad idea to require launching another service/application to get your unit tests to pass. This type of interaction is usually tested by mocking or vcring responses, or by creating environment tests that run against deployed servers. Launching another server is outside the scope of rspec and generally, as you've discovered, will cause a lot of headaches to setup and maintain.

However, if you're going to have these rails projects tightly coupled and you want them to share resources, I'd suggest investigating Rails Engines. To do this will require a substantial amount of work but the benefits can be quite high as the code will share a repository and have access to each other's capabilities, while maintaining application isolation.

Engines effectively create a rails application within another rails application. Each application has it's own namespace and a few isolating guards in place to prevent cross app contamination. If you have many engines it becomes ideal to have a shell rails application with minimal capabilities serving each engine on a different route/namespace.

First you need to create housing for the new api engine.

$ rails plugin new apiserver --mountable

This will provide you with lib/apiserver/engine.rb as well as all the other scaffolding you'll need to run your API as an engine. You'll also notice that config/routes.rb now has a route for your engine. You can copy your existing routes into this to provide a route path for your engine. All of your existing models will need to be moved into the namespace and you'll need to migrate any associated tables to the new naming convention. You'll also have some custom changes depending on your application and what you need to copy over to the engine, however the rails guide walks your through these changes (I won't enumerate all of them here).

It took a coworker about a week of work to get a complicated engine copied into another complicated rails server while development on both apps was occurring and with preserving version control history. A simpler app -- like an api only service -- I imagine would be quicker to establish.

What this gives you is another namespace scope at the application root. You can change this configuration around as you add more engines and shared code to match various other directory structures that make more sense.

app
     models
     ...
apiserver
     app
         ...

And once you've moved your code into the engine, you can test against your engine routers:

require "rails_helper"

describe APIServer::UsersController do
  routes { APIServer::Engine.routes }

  it "routes to the list of all users" do
    expect(:get => users_path).
      to route_to(:controller => "apiserver/users", :action => "index")
  end
end

You should be able to mix and match routes from both services and get cross-application testing done without launching a separate Rails app and without requiring an integration environment for your specs to pass.

Task rabbit has a great blog on how to enginize a rails application as a reference. They dive into the what to-do's and what not-to-do's in enginizing and go into more depth than can be easily transcribed to a SO post. I'd suggest following their procedure for engine decision making, though it's certainly not required to successfully enginize your api server.

Pyrce
  • 8,296
  • 3
  • 31
  • 46
0

You can stub requests like:

    stub_request(:get, %r{^#{ENV.fetch("BASE_URL")}/assets/email-.+\.css$})
Dorian
  • 7,749
  • 4
  • 38
  • 57