Skip to content

Commit

Permalink
refactor: drastically faster tests and better fixtures
Browse files Browse the repository at this point in the history
- unit tests 80% faster
- integration tests 60% faster
- Faker doesn't have a fixture table anymore, each test suite is
  responsible for creating its own fixtures.
- spec_helper has a helper method for fixture insertion
  • Loading branch information
thibaultcha committed May 18, 2015
1 parent bd9cc83 commit b095414
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 337 deletions.
4 changes: 4 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ files["kong/vendor/lapp.lua"] = {
ignore = {"lapp", "typespec"}
}

files["kong/api/app.lua"] = {
globals = {"jit"}
}

files["spec/"] = {
globals = {"describe", "it", "before_each", "setup", "after_each", "teardown", "stub", "mock", "spy", "finally"}
}
211 changes: 46 additions & 165 deletions kong/tools/faker.lua
Original file line number Diff line number Diff line change
@@ -1,92 +1,12 @@
local Object = require "classic"
local utils = require "kong.tools.utils"

math.randomseed(os.time())

-- Return a random element from an array.
-- @param `t` Array to get an element from.
-- @return A random element from the `t` array.
local function random_from_table(t)
if not t then return {} end
return t[math.random(#t)]
end

--
-- Faker
--
local Faker = Object:extend()

function Faker:new(dao_factory)
self.dao_factory = dao_factory
self.inserted_entities = {}
end

Faker.FIXTURES = {
api = {
-- TESTS APIs
{ name = "API TESTS 1", public_dns = "test1.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 2", public_dns = "test2.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 3", public_dns = "test3.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 4", public_dns = "test4.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 5", public_dns = "test5.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 6", public_dns = "cors1.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 7", public_dns = "cors2.com", target_url = "http://mockbin.com" },
{ name = "API TESTS 8 (logging)", public_dns = "logging.com", target_url = "http://mockbin.com" },

{ name = "API TESTS 8 (dns)", public_dns = "dns1.com", target_url = "http://127.0.0.1:7771" },
{ name = "API TESTS 9 (dns)", public_dns = "dns2.com", target_url = "http://localhost:7771" },

-- DEVELOPMENT APIs. Please do not use those in tests
{ name = "API DEV 1", public_dns = "dev.com", target_url = "http://mockbin.com" },
},
consumer = {
{ custom_id = "provider_123" },
{ custom_id = "provider_124" }
},
plugin_configuration = {
-- API 1
{ name = "keyauth", value = { key_names = { "apikey" }}, __api = 1 },
-- API 2
{ name = "basicauth", value = {}, __api = 2 },
-- API 3
{ name = "keyauth", value = {key_names = {"apikey"}, hide_credentials = true}, __api = 3 },
{ name = "ratelimiting", value = {period = "minute", limit = 6}, __api = 3 },
{ name = "ratelimiting", value = {period = "minute", limit = 8}, __api = 3, __consumer = 1 },
-- API 4
{ name = "ratelimiting", value = {period = "minute", limit = 6}, __api = 4 },
-- API 5
{ name = "request_transformer", value = {
add = { headers = {"x-added:true", "x-added2:true" },
querystring = {"newparam:value"},
form = {"newformparam:newvalue"} },
remove = { headers = { "x-to-remove" },
querystring = { "toremovequery" },
form = { "toremoveform" } } }, __api = 5 },
-- API 6
{ name = "cors", value = {}, __api = 6 },
-- API 7
{ name = "cors", value = { origin = "example.com",
methods = "GET",
headers = "origin, type, accepts",
exposed_headers = "x-auth-token",
max_age = 23,
credentials = true }, __api = 7 },
-- API 8
{ name = "tcplog", value = { host = "127.0.0.1", port = 7777 }, __api = 8 },
{ name = "udplog", value = { host = "127.0.0.1", port = 8888 }, __api = 8 },
{ name = "filelog", value = {}, __api = 8 },
},
-- TODO: remove plugins from core
keyauth_credential = {
{ key = "apikey122", __consumer = 1 },
{ key = "apikey123", __consumer = 2 }
},
basicauth_credential = {
{ username = "username", password = "password", __consumer = 1 }
}
}

-- Generate a fake entity
-- Generate a fake entity.
-- @param `type` Type of the entity to generate.
-- @return An valid entity (a table) complying to the defined schema.
function Faker:fake_entity(type)
Expand All @@ -103,117 +23,78 @@ function Faker:fake_entity(type)
custom_id = "random_custom_id_"..r
}
elseif type == "plugin_configuration" then
local plugin_type = random_from_table({ "keyauth", "ratelimiting" })
local plugin_value
if plugin_type == "keyauth" then
plugin_value = { key_names = { "apikey"..r }}
else
plugin_value = { period = "minute", limit = r }
end
return {
name = plugin_type,
value = plugin_value,
api_id = nil,
consumer_id = nil
}
-- TODO: remove plugins from core
elseif type == "basicauth_credential" then
return {
consumer_id = random_from_table(self.inserted_entities.consumer).id,
username = "username_random"..r,
password = "password_random"..r
}
elseif type == "keyauth_credential" then
return {
consumer_id = random_from_table(self.inserted_entities.consumer).id,
key = "key_random"..r
name = "keyauth",
value = { key_names = {"apikey"} }
}
else
error("Entity of type "..type.." cannot be generated.")
end
end

-- Seed the database with a set of hard-coded entities, and optionally random data
-- Seed the database with random APIs and Consumers.
-- @param {number} random_amount The number of random entities to add (apis, consumers, applications)
function Faker:seed(random_amount)
-- reset previously inserted entities
self.inserted_entities = {}

self:insert_from_table(utils.deepcopy(Faker.FIXTURES), true)

if random_amount then
-- If we ask for random entities, add as many random entities to another table
-- as the difference between total amount requested and hard-coded ones
-- If we ask for 1000 entities, we'll have (1000 - number_of_hard_coded) random entities
--
-- We don't generate any random plugin configuration
local random_entities = {}
for type, entities in pairs(Faker.FIXTURES) do
random_entities[type] = {}
if type ~= "plugin_configuration" then
for i = 1, random_amount do
table.insert(random_entities[type], self:fake_entity(type))
end
end
end
if not random_amount then random_amount = 0 end

local random_entities = {}

self:insert_from_table(random_entities)
for _, type in ipairs({ "api", "consumer" }) do
random_entities[type] = {}
for i = 1, random_amount do
table.insert(random_entities[type], self:fake_entity(type))
end
end

return self:insert_from_table(random_entities)
end

-- Insert entities in the DB using the DAO.
-- @param `entities_to_insert` A table with the same structure as the one defined in `:seed()`
-- @param `pick_relations` If true, will pick relations from the __ properties (see fixtures)
function Faker:insert_from_table(entities_to_insert, pick_relations)
function Faker:insert_from_table(entities_to_insert)
local inserted_entities = {}

-- Insert in order (for foreign relashionships)
-- 1. consumers and APIs
-- 2. credentials, which need refereces to inserted apis and consumers
-- 2. credentials, which need references to inserted apis and consumers
for _, type in ipairs({ "api", "consumer", "basicauth_credential", "keyauth_credential", "plugin_configuration" }) do
for i, entity in ipairs(entities_to_insert[type]) do

if pick_relations then
local foreign_api = entities_to_insert.api[entity.__api]
local foreign_consumer = entities_to_insert.consumer[entity.__consumer]

-- Clean this up otherwise won't pass schema validation
entity.__api = nil
entity.__consumer = nil

-- Hard-coded foreign relationships
if type == "basicauth_credential" then
if foreign_consumer then
entity.consumer_id = foreign_consumer.id
end
elseif type == "keyauth_credential" then
if foreign_consumer then
entity.consumer_id = foreign_consumer.id
end
elseif type == "plugin_configuration" then
if entities_to_insert[type] then
for i, entity in ipairs(entities_to_insert[type]) do

if entity.__api or entity.__consumer then
local foreign_api = entities_to_insert.api and entities_to_insert.api[entity.__api]
local foreign_consumer = entities_to_insert.consumer and entities_to_insert.consumer[entity.__consumer]

-- Clean this up otherwise won't pass schema validation
entity.__api = nil
entity.__consumer = nil

if foreign_api then entity.api_id = foreign_api.id end
if foreign_consumer then entity.consumer_id = foreign_consumer.id end
end
end

-- Insert in DB
local dao_type = type=="plugin_configuration" and "plugins_configurations" or type.."s"
local res, err = self.dao_factory[dao_type]:insert(entity)
if err then
local printable_mt = require "kong.tools.printable"
setmetatable(entity, printable_mt)
error("Faker failed to insert "..type.." entity: "..entity.."\n"..err)
end
-- Insert in DB
local dao_type = type=="plugin_configuration" and "plugins_configurations" or type.."s"
local res, err = self.dao_factory[dao_type]:insert(entity)
if err then
local printable_mt = require "kong.tools.printable"
setmetatable(entity, printable_mt)
error("Faker failed to insert "..type.." entity: "..entity.."\n"..err)
end

-- For other hard-coded entities relashionships
entities_to_insert[type][i] = res
-- For other hard-coded entities relashionships
entities_to_insert[type][i] = res

-- For generated fake_entities to fetch the relations they need
if not self.inserted_entities[type] then
self.inserted_entities[type] = {}
end
if not inserted_entities[type] then
inserted_entities[type] = {}
end

table.insert(self.inserted_entities[type], res)
table.insert(inserted_entities[type], res)
end
end
end

return inserted_entities
end

return Faker
2 changes: 1 addition & 1 deletion kong/tools/printable.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function printable_mt:__tostring()
v = table.concat(v, ",")
end

table.insert(t, k.."="..v)
table.insert(t, k.."="..tostring(v))
end
return table.concat(t, " ")
end
Expand Down
15 changes: 9 additions & 6 deletions spec/integration/cli/start_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ describe("CLI", function()
os.execute("cp "..TEST_CONF.." "..SERVER_CONF)
spec_helper.add_env(SERVER_CONF)
spec_helper.prepare_db(SERVER_CONF)
spec_helper.drop_db(SERVER_CONF) -- remove the seed from prepare_db()
end)

teardown(function()
Expand All @@ -34,7 +33,6 @@ describe("CLI", function()

after_each(function()
pcall(spec_helper.stop_kong, SERVER_CONF)
spec_helper.reset_db(SERVER_CONF)
end)

it("should start with the default configuration", function()
Expand Down Expand Up @@ -78,9 +76,16 @@ describe("CLI", function()
end)

it("should not work when a plugin is being used in the DB but it's not in the configuration", function()
replace_conf_property("plugins_available", {"keyauth", "basicauth", "tcplog", "udplog", "filelog", "request_transformer"})
spec_helper.get_env(SERVER_CONF).faker:insert_from_table {
api = {
{ name = "tests cli 1", public_dns = "foo.com", target_url = "http://mockbin.com" },
},
plugin_configuration = {
{ name = "ratelimiting", value = {period = "minute", limit = 6}, __api = 1 },
}
}

spec_helper.prepare_db(SERVER_CONF)
replace_conf_property("plugins_available", {"keyauth", "basicauth", "tcplog", "udplog", "filelog", "request_transformer"})

assert.has_error(function()
spec_helper.start_kong(SERVER_CONF, true)
Expand All @@ -90,8 +95,6 @@ describe("CLI", function()
it("should work the used plugins are enabled", function()
replace_conf_property("plugins_available", {"ratelimiting", "keyauth", "basicauth", "tcplog", "udplog", "filelog", "request_transformer", "cors"})

spec_helper.prepare_db(SERVER_CONF)

local _, exit_code = spec_helper.start_kong(SERVER_CONF, true)
assert.are.same(0, exit_code)
end)
Expand Down
Loading

0 comments on commit b095414

Please sign in to comment.