Как получить экземпляр приложения Sinatra, который тестируется в стойке?

Я хочу заполучить экземпляр приложения, тестируемый в стойке, чтобы можно было смоделировать некоторые из его методов. Я думал, что могу просто сохранить экземпляр приложения в app метод, но по какой-то странной причине, которая не работает. Это похоже на rack-test просто использует экземпляр, чтобы получить класс, а затем создает свой собственный экземпляр.

Я сделал тест, чтобы продемонстрировать мою проблему (для этого нужны гемы "sinatra", "rack-test" и "rr"):

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "instantiated app" do
  include Rack::Test::Methods

  def app
    cls = Class.new(Sinatra::Base) do
      get "/foo" do
        $instance_id = self.object_id

        generate_response
      end

      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am a response"]
      end
    end

    # Instantiate the actual class, and not a wrapped class made by Sinatra
    @app = cls.new!

    return @app
  end

  it "should have the same object id inside response handlers" do
    get "/foo"

    assert_equal $instance_id, @app.object_id,
      "Expected object IDs to be the same"
  end

  it "should trigger mocked instance methods" do
    mock(@app).generate_response {
      [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
    }

    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end

Как так rack-test не использует предоставленный мною экземпляр? Как мне получить экземпляр, который rack-test использует, так что я могу издеваться generate_response метод?


Обновить

Я не сделал никакого прогресса. Оказывается rack-test создает проверенный экземпляр на лету, когда сделан первый запрос (т.е. get("/foo")), так что до этого момента нельзя издеваться над экземпляром приложения.

Я использовал рр stub.proxy(...) перехватить .new, .new! а также .allocate; и добавил оператор put с именем класса экземпляра и object_id, Я также добавил такие операторы в конструктор тестируемого класса, а также в обработчик запросов.

Вот вывод:

Из конструктора: Прокси перехватил новый! экземпляр: Прокси перехватил новый экземпляр: Из обработчика запроса: 

Обратите внимание на идентификаторы объектов. Протестированный экземпляр (напечатанный из обработчика запроса) никогда не проходил .new и никогда не был инициализирован.

Так что, до некоторой степени, тестируемый экземпляр никогда не создается, но, тем не менее, существует. Я думаю, что allocate использовался, но перехват прокси показывает, что это не так. Я побежал TestSubject.allocate Я сам, чтобы проверить, что перехват работает, и это делает.

Я также добавил inherited, included, extended а также prepended привязывает к тестируемому классу и добавляет операторы print, но они никогда не вызывались. Это оставляет меня полностью и совершенно ошарашенным относительно того, какого рода ужасный тест черной магии готовится под капотом.

Итак, подведем итог: проверенный экземпляр создается на лету, когда отправляется первый запрос. Протестированный экземпляр создается магией Скверны и уклоняется от всех попыток поймать его с помощью крючка, поэтому я не могу найти способ его высмеять. Это почти похоже на автора rake-test сделал все возможное, чтобы исключить возможность прикосновения к экземпляру приложения во время тестирования.

Я все еще нащупываю решение.

2 ответа

Решение

Хорошо, я наконец получил это.

Проблема, все время, оказалась Sinatra::Base.call, Внутренне это делает dup.call!(env), Другими словами, каждый раз, когда вы бежите callSinatra продублирует экземпляр вашего приложения и отправит запрос на дубликаты, обходя все макеты и заглушки. Это объясняет, почему ни один из крючков жизненного цикла не сработал, так как предположительно dup использует некоторую магию низкого уровня C для клонирования экземпляра (необходима цитата).

rack-test не делает ничего замысловатого, все, что он вызывает app() чтобы получить приложение, а затем позвоните .call(env) в приложении. Все, что мне нужно сделать, это заглушить .call метод в моем классе и убедитесь, что магия Синатры нигде не вставлена. я могу использовать .new! в моем приложении, чтобы остановить Sinatra от вставки оболочки и стека, и я могу использовать .call! вызывать мое приложение без Синатры, дублирующего мой экземпляр приложения.

Примечание: я больше не могу просто создать анонимный класс внутри app функция, так как это будет создавать новый класс каждый раз app() называется и оставить меня неспособным насмехаться над этим.

Вот тест из вопроса, обновленный для работы:

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "sinatra app" do
  include Rack::Test::Methods

  class TestSubject < Sinatra::Base
    get "/foo" do
      generate_response
    end

    def generate_response
      [200, {"Content-Type" => "text/plain"}, "I am a response"]
    end
  end

  def app
    return TestSubject
  end

  it "should trigger mocked instance methods" do
    stub(TestSubject).call { |env|
      instance = TestSubject.new!

      mock(instance).generate_response {
        [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
      }

      instance.call! env
    }

    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end

Да, тестирование в стойке создает новые app за каждый запрос (возможно, чтобы избежать коллизий и начать с нового состояния.) Опция здесь будет посмеяться над Sinatra::Base сам производный класс, внутри app:

require "sinatra"
require "minitest/spec"
require "minitest/autorun"
require "rack/test"
require "rr"

describe "instantiated app" do
  include Rack::Test::Methods

  def app
    Class.new(Sinatra::Base) do
      get "/foo" do
        generate_response
      end

      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am a response"]
      end
    end.prepend(Module.new do # ⇐ HERE
      def generate_response
        [200, {"Content-Type" => "text/plain"}, "I am MOCKED"]
      end
    end).new!
  end

  it "should trigger mocked instance methods" do
    get "/foo"

    assert_equal "I am MOCKED", last_response.body
  end
end

или издеваться app метод в целом.

Другие вопросы по тегам