data_factory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # DataFactory
2
+
3
+ DataFactory is a simple Ruby gem that generates data random test data and inserts it into database tables. It was created to help with unit testing in a project that had database tables having many (50+) columns, and manually crafing insert statements was tedious and error prone.
4
+
5
+ DataFactory reads the table definition from the database, and generates random values for all not null columns. It inserts this data into the table, while providing the option of specifying non-random defaults.
6
+
7
+ In the 0.1 release DataFactory can be considered beta software. In other words it is not really production ready, but then as it generates random test data, it is not really designed to run on a production system, so that is probably OK. It is also missing many potential features, and could easily be enhanced to do much more.
8
+
9
+ # Database Compatibility
10
+
11
+ Right now, DataFactory only works with Oracle databases. It should be fairly easy to extend it to other databases, but all the DB interactions have not been abstracted into an access layer, so there would be some work to do.
12
+
13
+ # Requirements
14
+
15
+ There are no strict dependencies for DataFactory to work, and it is a pure Ruby gem.
16
+
17
+ However, DataFactory doesn't actually have a database access layer built in, as it is designed to use an external access layer that knows how to connect and query the database.
18
+
19
+ If you can use JRuby, consider using Simple JDBC Oracle to interact with your database. If you cannot use JRuby, implementing your own database interface is simple. Create a class that handles creating a database connection, and implement the following three methods to run SQL statements on the database, and issue commits:
20
+
21
+ # should return an object that implements the each_array method
22
+ # below
23
+ def execute_sql(statement, *binds)
24
+ end
25
+
26
+ def commit
27
+ end
28
+
29
+ def each_array(&blk)
30
+ end
31
+
32
+ The first two methods are fairly obvious. The each_array method is expected to iterate over the result set returned by the executed sql statement, passing an array of columns to the block for each row returned. The array should contain the value of each column in Ruby types, ie not Java types if using JRuby.
33
+
34
+ The OCI8 gem is pretty good place to start if you are using MRI Ruby, but you will need the Oracle client installed. Raw JDBC can get the job done if you are using JRuby and you do not want to use Simple JDBC Oracle.
35
+
36
+ # Usage
37
+
38
+ DataFactory is a simple gem, so a few examples explore a lot of the functionality. Note that these examples use Simple Oracle JDBC as the database access layer.
39
+
40
+ For these examples to run, create a table on the database as follows:
41
+
42
+ create table employees (emp_id integer,
43
+ dept_id integer,
44
+ first_name varchar2(50),
45
+ last_name varchar2(50),
46
+ email varchar2(50),
47
+ ssn varchar2(10));
48
+
49
+ ## Define a DataFactory Class
50
+
51
+ To use DataFactory, create a class for each table you want to interface with, and make it a sub-class of DataFactory::Base:
52
+
53
+ class Employee < DataFactory::Base
54
+
55
+ set_table_name "employees"
56
+
57
+ set_column_default :last_name, "Smith"
58
+ set_column_default :email, begin
59
+ "#{rand(10000)}@#{rand(10000)}.com"
60
+ end
61
+ end
62
+
63
+ In the class definition, use the set_table_name method to map the class to a particular table on the database.
64
+
65
+ Optionally, you can specify default values for columns in the table with the set_column_default method, which takes the table name followed by a value for the column, or a block that generates the value each time it is called, as with the email example.
66
+
67
+
68
+ ## Creating a Row
69
+
70
+ The first requirement is to connect to the database, and hand an instance of the database interface to DataFactory:
71
+
72
+ interface = SimpleOracleJDBC::Interface.create('sodonnel',
73
+ 'sodonnel',
74
+ 'local11gr2.world',
75
+ 'localhost',
76
+ '1521')
77
+
78
+ DataFactory::Base.set_database_interface(interface)
79
+
80
+ Then a row can be created using the create! method, for example:
81
+
82
+ f = Employee.create!("emp_id" => 1001)
83
+
84
+ The create! call will take the column defaults defined in the Employee class, and merge in any column values passed into the create! method. Then it will generate a value for any other non-nullable columns in the table, and insert the row into the database.
85
+
86
+ An Employee instance is returned, containing all the generated values.
87
+
88
+ ## Putting It Together
89
+
90
+ Combining each of the steps above, gives the following script:
91
+
92
+ require 'rubygems'
93
+ require 'simple_oracle_jdbc'
94
+ require 'data_factory'
95
+
96
+ class Employee < DataFactory::Base
97
+
98
+ set_table_name "employees"
99
+
100
+ set_column_default :last_name, "Smith"
101
+ set_column_default :email, begin
102
+ "#{rand(10000)}@#{rand(10000)}.com"
103
+ end
104
+ end
105
+
106
+ interface = SimpleOracleJDBC::Interface.create('sodonnel',
107
+ 'sodonnel',
108
+ 'local11gr2.world',
109
+ 'localhost',
110
+ '1521')
111
+
112
+ DataFactory::Base.set_database_interface(interface)
113
+
114
+ f = Employee.create!("emp_id" => 1001)
115
+
116
+ f.column_values.keys.each do |k|
117
+ puts f.column_values[k]
118
+ end
119
+
120
+ ## Other Methods
121
+
122
+ The sample above illustrates how the create! method returns an Employee object, giving access to the generated values. For an overview of other methods browse the documentation for the base_api, base_dsl and base_factory methods.
123
+
124
+
125
+
126
+
127
+
128
+
129
+
data/Rakefile.rb ADDED
@@ -0,0 +1,30 @@
1
+ desc "Generate RDoc"
2
+ task :doc => ['doc:generate']
3
+
4
+ namespace :doc do
5
+ project_root = File.expand_path(File.dirname(__FILE__))
6
+ doc_destination = File.join(project_root, 'doc')
7
+
8
+ begin
9
+ require 'yard'
10
+ require 'yard/rake/yardoc_task'
11
+
12
+ YARD::Rake::YardocTask.new(:generate) do |yt|
13
+ yt.files = Dir.glob(File.join(project_root, 'lib/**/*.rb')) + #, '**', '*.rb')) +
14
+ [ File.join(project_root, 'README.md') ]
15
+ puts yt.files
16
+ yt.options = ['--output-dir', doc_destination, '--readme', 'README.md']
17
+ end
18
+ rescue LoadError
19
+ desc "Generate YARD Documentation"
20
+ task :generate do
21
+ abort "Please install the YARD gem to generate rdoc."
22
+ end
23
+ end
24
+
25
+ desc "Remove generated documenation"
26
+ task :clean do
27
+ rm_r doc_dir if File.exists?(doc_destination)
28
+ end
29
+
30
+ end
@@ -0,0 +1,12 @@
1
+ require 'date'
2
+
3
+ require 'data_factory/random'
4
+ require 'data_factory/exceptions'
5
+ require 'data_factory/column'
6
+ require 'data_factory/base_dsl'
7
+ require 'data_factory/base_factory'
8
+ require 'data_factory/base_api'
9
+
10
+ require 'data_factory/base'
11
+
12
+
@@ -0,0 +1,16 @@
1
+ module DataFactory
2
+
3
+ # The Base class provides a class designed to be sub-classed. It does not create any
4
+ # methods on its own, but is extended with class methods from the BaseDSL and BaseFactory
5
+ # modules and has the BaseAPI module included to provide instance methods.
6
+
7
+ class Base
8
+
9
+ extend BaseDSL
10
+ extend BaseFactory
11
+
12
+ include BaseAPI
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,245 @@
1
+ module DataFactory
2
+
3
+ # This module is included into the DataFactory::Base class providing methods
4
+ # to generate data and insert it into the database.
5
+ #
6
+ # For most use cases, these methods will not need to be called. The
7
+ # DataFactory::BaseFactory module provides factory methods that create objects and insert
8
+ # the data into database
9
+
10
+ module BaseAPI
11
+
12
+ include Random
13
+
14
+ attr_reader :column_values, :insert_statement, :binds
15
+
16
+ def initialize
17
+ unless self.class.meta_data_loaded
18
+ self.class.load_meta_data
19
+ end
20
+ @column_values = Hash.new
21
+ end
22
+
23
+ # Retrieves the table name this class interfaces with on the database
24
+ def table_name
25
+ self.class.table_name
26
+ end
27
+
28
+ def column_details # :nodoc:
29
+ self.class.column_details
30
+ end
31
+
32
+ def column_defaults # :nodoc:
33
+ self.class.column_defaults
34
+ end
35
+
36
+ def column_detail(key) # :nodoc:
37
+ self.class.column_detail(key)
38
+ end
39
+
40
+ # Retrieves the default value set for a column, which can be a proc
41
+ # or any Ruby object in general, but in practice is likely to be a
42
+ # Date, Time, String, Float, Integer
43
+ def column_default(key)
44
+ self.class.column_default(key)
45
+ end
46
+
47
+ # Returns the value assigned to a column, or nil if it is not defined
48
+ def column_value(key)
49
+ # ensure the requested column is in the table
50
+ column_detail(key)
51
+ @column_values[key.to_s.upcase]
52
+ end
53
+
54
+ # Returns the database interface object
55
+ def db_interface
56
+ self.class.database_interface
57
+ end
58
+
59
+ # Commit changes to the database
60
+ def commit
61
+ db_interface.commit
62
+ end
63
+
64
+ # Generates values for all the columns in the table.
65
+ #
66
+ # If no value is passed into this procedure for a column, a default will be used
67
+ # if it is configured. Otherwise a random value will be generated.
68
+ #
69
+ # Values for a column can be passed into this method using a hash, eg:
70
+ #
71
+ # obj.generate_column_data(:emp_id => 1, :last_name => 'Smith)
72
+ def generate_column_data(params=Hash.new)
73
+ self.class.column_details.keys.each do |k|
74
+ if column_default(k)
75
+ if column_default(k).is_a?(Proc)
76
+ @column_values[k] = column_default(k).call
77
+ else
78
+ @column_values[k] = column_default(k)
79
+ end
80
+ else
81
+ @column_values[k] = generate_value(column_detail(k))
82
+ end
83
+ end
84
+ normalised_params = internalize_column_params(params)
85
+ validate_columns(normalised_params)
86
+ @column_values.merge! normalised_params
87
+ end
88
+
89
+
90
+ # Generates an insert statement for the current state of the object. It is intended
91
+ # to be called after generate_column_data has been called, otherwise all the values
92
+ # will be null.
93
+ #
94
+ # The generated statement returned as a string and also stored in the @insert_statement
95
+ # instance variable.
96
+ #
97
+ # The insert statement will contain bind variables for all columns. The generated bind
98
+ # variabled are stored in @binds.
99
+ def generate_insert
100
+ @binds = Array.new
101
+ @insert_statement = "insert into #{table_name} ("
102
+ @insert_statement << column_details.keys.sort.map { |k| column_detail(k).column_name }.join(',')
103
+ @insert_statement << ') values ('
104
+ @insert_statement << column_details.keys.sort.map { |k|
105
+ ":#{k}"
106
+ }.join(',')
107
+ column_details.keys.sort.each { |k|
108
+ if @column_values[k] == nil
109
+ @binds.push [column_type_to_ruby_type(column_details[k]), nil]
110
+ else
111
+ @binds.push @column_values[k]
112
+ end
113
+ }
114
+ @insert_statement << ')'
115
+ @insert_statement
116
+ end
117
+
118
+ # Runs the insert statement prepared by generate_insert, using the insert
119
+ # statement stored in @insert_statement and the bind variables in @binds
120
+ #
121
+ # If generate_insert has not be executed, this procedure will raise
122
+ # a DataFactory::NoInsertStatement exeception
123
+ def run_insert
124
+ raise DataFactory::NoInsertStatement unless @insert_statement
125
+
126
+ stmt = db_interface.execute_sql(@insert_statement, *@binds)
127
+ stmt.close
128
+ end
129
+
130
+ # Generates a where clause for the DataFactory object. This method looks at the columns
131
+ # on the table, and the values set against them, and generates a string representing a
132
+ # where clause that can be used in an SQL query.
133
+ #
134
+ # The generated string does not contain the 'where' keyword.
135
+ def where_clause_for(cols)
136
+ cols.map{|c| c.to_s.upcase}.map{|c|
137
+ if @column_values[c] == nil
138
+ "#{c} is null"
139
+ else
140
+ "#{c} = #{quote_value(c) }"
141
+ end
142
+ }.join " and "
143
+ end
144
+
145
+ private
146
+
147
+ # TODO - This needs to be extracted into a DB specific access layer, as it is
148
+ # Oracle specific.
149
+ def quote_value(col)
150
+ case column_detail(col).data_type
151
+ when 'CHAR', 'VARCHAR2', 'CLOB'
152
+ "'#{@column_values[col]}'"
153
+ when 'DATE', 'DATETIME'
154
+ "to_date('#{@column_values[col].strftime('%Y%m%d %H:%M:%S')}', 'YYYYMMDD HH24:MI:SS')"
155
+ else
156
+ @column_values[col].to_s
157
+ end
158
+ end
159
+
160
+ def generate_value(col)
161
+ if col.nullable?
162
+ return nil
163
+ end
164
+
165
+ case col.data_type
166
+ when 'CHAR', 'VARCHAR2', 'CLOB'
167
+ random_string_upto_length(col.data_length)
168
+ when 'DATE', 'DATETIME', 'TIMESTAMP'
169
+ Time.now
170
+ when 'NUMBER', 'INTEGER'
171
+ scale = 2
172
+ if col.data_scale && col.data_scale == 0
173
+ 1
174
+ else
175
+ 22.23
176
+ end
177
+
178
+ # # random numbers is very much beta and very untested.
179
+ # #
180
+ # # 38 digits total, 28 before decimal point, and 10 after.
181
+ # precision = 38
182
+ # scale = 10
183
+ # if col.data_precision
184
+ # precision = col.data_precision
185
+ # end
186
+ # if col.data_scale
187
+ # scale = col.data_scale
188
+ # end
189
+
190
+ # if scale == 0
191
+ # # its an integer
192
+ # random_integer(10**col.data_precision - 1)
193
+ # else
194
+ # # its a number
195
+ # max_left = 10**(precision - scale) - 1
196
+ # max_right = 10**scale - 1
197
+ # "#{random_integer(max_left)}.#{random_integer(max_right)}".to_f
198
+ # end
199
+ when 'NUMBER'
200
+ 23.34
201
+ when 'INTEGER'
202
+ else
203
+ nil
204
+ end
205
+ end
206
+
207
+ def column_type_to_ruby_type(col)
208
+ case col.data_type
209
+ when 'CHAR', 'VARCHAR2', 'CLOB'
210
+ String
211
+ when 'DATE', 'DATETIME', 'TIMESTAMP'
212
+ Time
213
+ when 'INTEGER'
214
+ Integer
215
+ when 'NUMBER'
216
+ if col.data_scale == 0
217
+ Integer
218
+ else
219
+ Float
220
+ end
221
+ else
222
+ raise "unsupported datatype #{col.data_type}"
223
+ end
224
+ end
225
+
226
+ def internalize_column_params(params)
227
+ upcased_params = Hash.new
228
+ params.keys.each do |k|
229
+ # change it from a symbol(if it is a symbol) to a string and uppercase.
230
+ upcased_params[k.to_s.upcase] = params[k]
231
+ end
232
+ upcased_params
233
+ end
234
+
235
+ def validate_columns(params)
236
+ params.keys.each do |k|
237
+ unless column_details.has_key? k.upcase
238
+ raise DataFactory::ColumnNotInTable
239
+ end
240
+ end
241
+ end
242
+
243
+
244
+ end
245
+ end
@@ -0,0 +1,175 @@
1
+ module DataFactory
2
+
3
+ # This module is used to extend the DataFactory::Base class, and so creates a
4
+ # series of class instance methods that can be used in class definitions.
5
+ #
6
+ # For example:
7
+ #
8
+ # class Foo < DataFactory::Base
9
+ #
10
+ # set_table_name "Foobar"
11
+ # set_column_default :last_name, 'Smith'
12
+ #
13
+ # end
14
+ #
15
+ # For this reason, calling any of these methods on a class, will affect ALL instances
16
+ # of that class.
17
+ #
18
+ # Any subclasses expect the class variables @@db_interface to be set, containing a database
19
+ # access class, eg:
20
+ #
21
+ # DataFactory::Base.set_database_interface(interface_obj)
22
+ #
23
+ # As this is stored in a class variable the same database interface is shared down the entire
24
+ # inheritance chain. Therefore if many database connections are required, several base classes
25
+ # will need to be created by including the relevant modules, eg:
26
+ #
27
+ # class OtherBaseClass
28
+ # extend BaseDSL
29
+ # extend BaseFactory
30
+ # include BaseAPI
31
+ # end
32
+
33
+ module BaseDSL
34
+
35
+ attr_reader :table_name, :column_details, :column_defaults, :meta_data_loaded
36
+
37
+ # Pass a database interface object to be used by all DataFactory sub-classes
38
+ # The interface must implement the following two methods:
39
+ #
40
+ # def execute_sql(statement, *binds)
41
+ # end
42
+ #
43
+ # def each_array(&blk)
44
+ # end
45
+ #
46
+ # def commit
47
+ # end
48
+ #
49
+ # This method is normally called on the DataFactory::Base class. It will
50
+ # set a class variable (@@db_interface) in the Base class which is shared
51
+ # by all sub-classes of the class.
52
+ #
53
+ # Ideally use the SimpleOracleJDBC gem to provide the interface
54
+ def set_database_interface(interface)
55
+ class_variable_set("@@db_interface", interface)
56
+ end
57
+
58
+
59
+ # Returns the database interface that was set by set_database_interface
60
+ def database_interface
61
+ class_variable_get("@@db_interface")
62
+ end
63
+
64
+
65
+ # Defines the table a subclass of DataFactory interacts with on the database. This
66
+ # method stores the tables in a class instance variable (@table_name) that is shared by
67
+ # all instances of the class, but is not inherited with subclasses.
68
+ def set_table_name(tab)
69
+ @table_name = tab.to_s.upcase
70
+ end
71
+
72
+ # Sets the default value to be used for a column if nothing else is
73
+ # specified. The block is optional and should be used if dynamic defaults
74
+ # are required. If the default is a constant, a simple value can be specified.
75
+ # The default is stored within a hash which is stored in a class instance variable
76
+ # (@column_defaults) that is shared by all instances of the class, but is not inherited
77
+ # with subclasses.
78
+ def set_column_default(column_name, value=nil, &block)
79
+ unless defined? @column_defaults
80
+ @column_defaults = Hash.new
81
+ end
82
+ @column_defaults[column_name.to_s.upcase] = block_given? ? block :
83
+ value.is_a?(Symbol) ? value.to_s : value
84
+ end
85
+
86
+
87
+ # Returns the value for a column default. If it is a simple value, the value
88
+ # is returned. If the default is a proc, the proc is executed and the resulting
89
+ # values is returned.
90
+ def column_default(column_name)
91
+ return nil unless defined? @column_defaults
92
+
93
+ val = @column_defaults[column_name.to_s.upcase]
94
+ if val.is_a?(Proc)
95
+ val.call
96
+ else
97
+ val
98
+ end
99
+ end
100
+
101
+
102
+ # Returns a DataFactory::Column object that defines the properties
103
+ # of the column
104
+ def column_detail(column_name)
105
+ load_meta_data unless meta_data_loaded
106
+
107
+ unless @column_details.has_key?(column_name.to_s.upcase)
108
+ raise DataFactory::ColumnNotInTable
109
+ end
110
+
111
+ @column_details[column_name.to_s.upcase]
112
+ end
113
+
114
+
115
+ # Used to load the table meta-data from the database
116
+ # TODO - abstract into a database layer as this code is Oracle specific
117
+ def load_meta_data # :nodoc:
118
+ raise DataFactory::TableNotSet unless @table_name
119
+ raise DataFactory::DatabaseInterfaceNotSet unless class_variable_defined?("@@db_interface")
120
+
121
+ if meta_data_loaded
122
+ return
123
+ end
124
+
125
+ @column_details = Hash.new
126
+
127
+ unless defined? @column_defaults
128
+ @column_defaults = Hash.new
129
+ end
130
+
131
+ table_details_sql = "select column_name,
132
+ data_type,
133
+ data_length,
134
+ data_precision,
135
+ data_scale,
136
+ column_id,
137
+ nullable
138
+ from user_tab_columns
139
+ where table_name = ?
140
+ order by column_id asc"
141
+
142
+ database_interface.execute_sql(table_details_sql, @table_name).each_array do |r|
143
+ c = Column.new
144
+ c.column_name = r[0].upcase
145
+ c.data_type = r[1].upcase
146
+ c.data_length = r[2].to_i
147
+ c.data_precision = r[3].to_i
148
+ c.data_scale = r[4].to_i
149
+ c.position = r[5].to_i
150
+ c.nullable = r[6] == 'N' ? false : true
151
+ @column_details[r[0].upcase] = c
152
+ end
153
+ @meta_data_loaded = true
154
+ # This is needed here as some column defaults will have been set
155
+ # before the meta_data was loaded and hence will not have been checked
156
+ validate_column_defaults
157
+ end
158
+
159
+ private
160
+
161
+ def validate_column_defaults
162
+ @column_defaults.keys.each do |k|
163
+ validate_column_default(k, @column_defaults[k])
164
+ end
165
+ end
166
+
167
+
168
+ def validate_column_default(column_name, column_value)
169
+ unless @column_details.has_key? column_name
170
+ raise DataFactory::ColumnNotInTable, column_name
171
+ end
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,41 @@
1
+ module DataFactory
2
+ module BaseFactory
3
+
4
+ # This module implements the class methods of the DataFactory::Base class.
5
+ # These factory methods should be used to generate database records.
6
+
7
+ # Builds a new instance of a DataFactory class, inserts it into the database
8
+ # and commits the transaction. The params parameter is expected to be a hash, mapping columns
9
+ # in the underlying table to values. These values will be merged with any
10
+ # defaults set for the columns, and will override the defaults if passed. For example:
11
+ #
12
+ # obj = Employee.create!(:last_name => 'Smith')
13
+ def create!(params)
14
+ df = create(params)
15
+ df.commit
16
+ df
17
+ end
18
+
19
+ # Same as the create! method, only the transaction is not committed on the database.
20
+ # For example:
21
+ #
22
+ # obj = Employee.create(:last_name => 'Smith')
23
+ def create(params)
24
+ df = build(params)
25
+ df.generate_insert
26
+ df.run_insert
27
+ df
28
+ end
29
+
30
+ # Builds and returns a new instance of a DataFactory class, but does not insert it
31
+ # into the database. For example:
32
+ #
33
+ # obj = Employee.build(:last_name => 'Smith')
34
+ def build(params)
35
+ df = self.new
36
+ df.generate_column_data(params)
37
+ df
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ module DataFactory
2
+ class Column
3
+
4
+ attr_accessor :column_name, :data_type, :data_length, :data_scale, :data_precision, :position, :nullable
5
+
6
+ def initialize
7
+ end
8
+
9
+ def nullable?
10
+ nullable
11
+ end
12
+
13
+ def to_s
14
+ "#{@column_name} #{@data_type} #{data_length} #{data_scale} #{data_precision} #{position.to_s}"
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module DataFactory
2
+ class TableNotSet < Exception; end
3
+ class DatabaseInterfaceNotSet < Exception; end
4
+ class ColumnNotInTable < Exception; end
5
+ class NoInsertStatement < Exception; end;
6
+ end
@@ -0,0 +1,28 @@
1
+ module DataFactory
2
+ module Random
3
+
4
+ CHARS = ['A'..'Z', 'a'..'z', '0'..'9'].map{|r|r.to_a}.flatten
5
+
6
+ # Generates a random string of letters and numbers of length
7
+ def random_string_of_length(length)
8
+ str = ''
9
+ 1.upto(length) do
10
+ str << CHARS[rand(CHARS.size)]
11
+ end
12
+ str
13
+ end
14
+
15
+ # Generates a random string of random length, which has a maximum
16
+ # length of max_length
17
+ def random_string_upto_length(max_length)
18
+ length = random_integer(max_length)
19
+ random_string_of_length(length)
20
+ end
21
+
22
+ # Generates a random integer that is at most max_size
23
+ def random_integer(max_size)
24
+ 1 + rand(max_size)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,151 @@
1
+ require 'helper'
2
+
3
+ class BaseAPITest < Test::Unit::TestCase
4
+
5
+ include TestHelper
6
+
7
+ def setup
8
+ @klass = Class.new
9
+ @klass.extend DataFactory::BaseDSL
10
+ @dbinterface = DBInterfaceMock.new
11
+ @klass.set_database_interface(@dbinterface)
12
+ @klass.set_table_name('foobar')
13
+ @klass.send :include, DataFactory::BaseAPI
14
+ end
15
+
16
+ def teardown
17
+ end
18
+
19
+ def test_exception_if_table_name_not_set
20
+ klass = Class.new
21
+ klass.extend DataFactory::BaseDSL
22
+ dbinterface = DBInterfaceMock.new
23
+ klass.set_database_interface(dbinterface)
24
+ klass.send :include, DataFactory::BaseAPI
25
+ assert_raises DataFactory::TableNotSet do
26
+ instance = klass.new
27
+ end
28
+ end
29
+
30
+ def test_table_name_can_be_retrieved
31
+ instance = @klass.new
32
+ assert_equal('FOOBAR', instance.table_name)
33
+ end
34
+
35
+ def test_column_default_is_retrieved
36
+ @klass.set_column_default('COL1', 'abcd')
37
+ instance = @klass.new
38
+ assert_equal('abcd', instance.column_default('COL1'))
39
+ end
40
+
41
+ def test_column_default_is_nil_if_not_set
42
+ instance = @klass.new
43
+ assert_equal(nil, instance.column_default('COL1'))
44
+ end
45
+
46
+ def test_column_default_hash_is_retrieved
47
+ @klass.set_column_default('COL1', 'abcd')
48
+ instance = @klass.new
49
+ assert(instance.column_defaults.is_a?(Hash))
50
+ assert_equal('abcd', instance.column_defaults['COL1'])
51
+ end
52
+
53
+ def test_empty_default_hash_retrieved_if_no_defaults
54
+ instance = @klass.new
55
+ assert(instance.column_defaults.is_a?(Hash))
56
+ assert_equal(0, instance.column_defaults.keys.length)
57
+ end
58
+
59
+ def test_column_detail_is_retrieved
60
+ instance = @klass.new
61
+ assert(instance.column_detail('COL1').is_a? DataFactory::Column)
62
+ assert_equal('COL1', instance.column_detail('COL1').column_name)
63
+ end
64
+
65
+ def test_exception_raised_if_column_not_in_table
66
+ instance = @klass.new
67
+ assert_raises DataFactory::ColumnNotInTable do
68
+ instance.column_detail('not exist')
69
+ end
70
+ end
71
+
72
+ def test_column_detail_hash_retrieved
73
+ instance = @klass.new
74
+ assert(instance.column_details.is_a? Hash)
75
+ assert_equal('COL1', instance.column_details['COL1'].column_name)
76
+ end
77
+
78
+ def test_db_interface_returned
79
+ instance = @klass.new
80
+ assert_equal(@dbinterface, instance.db_interface)
81
+ end
82
+
83
+ def test_commit_called_on_db_interface
84
+ # Mocha will raise an exception if the commit method is not invoked
85
+ instance = @klass.new
86
+ @dbinterface.expects(:commit).at_least_once
87
+ instance.commit
88
+ end
89
+
90
+ def test_column_value_returns_nil_when_not_generated
91
+ instance = @klass.new
92
+ assert_equal(nil, instance.column_value('COL1'))
93
+ end
94
+
95
+ def test_execption_raised_for_column_value_when_not_in_table
96
+ instance = @klass.new
97
+ assert_raises DataFactory::ColumnNotInTable do
98
+ instance.column_value('NOTTHERE')
99
+ end
100
+ end
101
+
102
+ def test_generate_data_generates_nil_if_column_nullable
103
+ instance = @klass.new
104
+ instance.generate_column_data
105
+ assert_nil(instance.column_value('COL1'))
106
+ end
107
+
108
+ def test_generate_data_generates_column_data_if_column_not_nullable
109
+ instance = @klass.new
110
+ instance.generate_column_data
111
+ assert_not_nil(instance.column_value('COL3'))
112
+ end
113
+
114
+ def test_generate_data_default_used_for_column_when_present
115
+ @klass.set_column_default :col1, 'PRESET'
116
+ instance = @klass.new
117
+ instance.generate_column_data
118
+ assert_equal('PRESET', instance.column_value('COL1'))
119
+ end
120
+
121
+ def test_generate_data_passed_in_value_overrides_default
122
+ @klass.set_column_default :col1, 'PRESET'
123
+ instance = @klass.new
124
+ instance.generate_column_data(:col1 => 'OVERRIDE')
125
+ assert_equal('OVERRIDE', instance.column_value('COL1'))
126
+ end
127
+
128
+ def test_generate_data_raises_exception_when_passed_in_column_not_exist
129
+ instance = @klass.new
130
+ assert_raises DataFactory::ColumnNotInTable do
131
+ instance.generate_column_data(:notexist => 'OVERRIDE')
132
+ end
133
+ end
134
+
135
+
136
+ # TODO - tests for generate insert
137
+ # TODO - tests for
138
+
139
+
140
+
141
+
142
+ def test_run_insert_invokes_execute_on_db_interface
143
+ instance = @klass.new
144
+ instance.generate_column_data
145
+ instance.generate_insert
146
+ @dbinterface.expects(:execute_sql).returns(@dbinterface)
147
+ instance.run_insert
148
+ end
149
+
150
+ end
151
+
@@ -0,0 +1,109 @@
1
+ require 'helper'
2
+
3
+ class BaseDSLTest < Test::Unit::TestCase
4
+
5
+ include TestHelper
6
+
7
+ def setup
8
+ @klass = Class.new
9
+ @klass.extend DataFactory::BaseDSL
10
+ end
11
+
12
+ def teardown
13
+ end
14
+
15
+ def test_table_name_can_be_set_as_a_class_instance_variable
16
+ @klass.set_table_name('foobar')
17
+ assert_equal('FOOBAR', @klass.table_name)
18
+ assert_equal('FOOBAR', @klass.instance_variable_get('@table_name'))
19
+ end
20
+
21
+ def test_table_name_as_symbol_converted_to_string
22
+ @klass.set_table_name(:foobar)
23
+ assert_equal('FOOBAR', @klass.table_name)
24
+ end
25
+
26
+ def test_db_interface_is_set_as_a_class_variable
27
+ dbklass = Class.new
28
+ @klass.set_database_interface(dbklass)
29
+ # need to use send here as @@db_interface is a private variable
30
+ assert_equal(dbklass, @klass.send(:class_variable_get, '@@db_interface'))
31
+ end
32
+
33
+ def test_set_column_default_value
34
+ @klass.set_column_default("COL1", 'value1')
35
+ assert_equal('value1', @klass.column_default('COL1'))
36
+ end
37
+
38
+ def test_set_column_default_as_symbol_converted_to_string
39
+ @klass.set_column_default(:col1, 'value1')
40
+ assert_equal('value1', @klass.column_default('COL1'))
41
+ end
42
+
43
+ def test_set_column_default_as_proc
44
+ @klass.set_column_default(:col1) { 'value1' }
45
+ assert_equal('value1', @klass.column_default('COL1'))
46
+ end
47
+
48
+ def test_set_column_default_value_as_symbol_converted_to_string
49
+ @klass.set_column_default("COL1", :value1)
50
+ assert_equal('value1', @klass.column_default('COL1'))
51
+ end
52
+
53
+ def test_get_column_default_returns_nil_when_not_set
54
+ assert_equal(nil, @klass.column_default('COL1'))
55
+ end
56
+
57
+ def test_get_column_default_as_symbol_converted_to_string
58
+ @klass.set_column_default("COL1", 'value1')
59
+ assert_equal('value1', @klass.column_default(:col1))
60
+ end
61
+
62
+ def test_load_meta_data_loads_columns
63
+ @klass.set_database_interface(DBInterfaceMock.new)
64
+ @klass.set_table_name(:foobar)
65
+ @klass.load_meta_data
66
+ assert(@klass.column_detail('COL1').is_a?(DataFactory::Column))
67
+ assert_equal('COL1', @klass.column_detail('COL1').column_name)
68
+ end
69
+
70
+ def test_load_meta_data_exception_raised_when_table_not_set
71
+ assert_raises DataFactory::TableNotSet do
72
+ @klass.set_database_interface(DBInterfaceMock.new)
73
+ @klass.load_meta_data
74
+ end
75
+ end
76
+
77
+ def test_load_meta_data_exception_raised_when_db_interface_not_set
78
+ @klass.set_table_name(:foobar)
79
+ assert_raises DataFactory::DatabaseInterfaceNotSet do
80
+ @klass.load_meta_data
81
+ end
82
+ end
83
+
84
+ def test_load_meta_data_raises_exception_when_invalid_column_default_set
85
+ @klass.set_database_interface(DBInterfaceMock.new)
86
+ @klass.set_table_name(:foobar)
87
+ @klass.set_column_default :not_exists, :value1
88
+ assert_raises DataFactory::ColumnNotInTable do
89
+ @klass.load_meta_data
90
+ end
91
+ end
92
+
93
+ def test_column_detail_loads_meta_data_if_not_loaded
94
+ @klass.set_database_interface(DBInterfaceMock.new)
95
+ @klass.set_table_name(:foobar)
96
+ assert(@klass.column_detail('COL1').is_a?(DataFactory::Column))
97
+ end
98
+
99
+ def test_column_detail_raises_exception_if_column_does_not_exist
100
+ @klass.set_database_interface(DBInterfaceMock.new)
101
+ @klass.set_table_name(:foobar)
102
+ assert_raises DataFactory::ColumnNotInTable do
103
+ assert(@klass.column_detail('COLNOTTHERE').is_a?(DataFactory::Column))
104
+ end
105
+ end
106
+
107
+
108
+ end
109
+
data/test/helper.rb ADDED
@@ -0,0 +1,49 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ $:.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ #require 'rubygems'
5
+ #require 'simple_oracle_jdbc'
6
+ require 'data_factory'
7
+ require 'test/unit'
8
+ require 'mocha/setup'
9
+
10
+ module TestHelper
11
+
12
+ DB_USER = 'sodonnel'
13
+ DB_PASSWORD = 'sodonnel'
14
+ DB_SERVICE = 'local11gr2.world'
15
+ DB_HOST = 'localhost'
16
+ DB_PORT = '1521'
17
+
18
+ # @@interface ||= SimpleOracleJDBC::Interface.create(DB_USER,
19
+ # DB_PASSWORD,
20
+ # DB_SERVICE,
21
+ # DB_HOST,
22
+ # DB_PORT)
23
+
24
+ class DBInterfaceMock
25
+ def execute_sql(sql, binds)
26
+ self
27
+ end
28
+
29
+ # Mock out returing a list of columns for a table.
30
+ def each_array(&blk)
31
+ data = [
32
+ # cname, type len precision, scale, position, nullable
33
+ ['col1', 'varchar2', 20, nil, nil, 1, 'Y'],
34
+ ['col2', 'number', 20, 9, 2, 2, 'Y'],
35
+ ['col3', 'DATE', 20, nil, nil, 3, 'N'],
36
+ ['col4', 'varchar2', 20, nil, nil, 4, 'N'],
37
+ ['col5', 'integer', 20, 38, 0, 5, 'N'],
38
+ ['col6', 'number', 20, 20, 5, 6, 'N']
39
+ ]
40
+ data.each do |d|
41
+ yield d
42
+ end
43
+ end
44
+
45
+ def close
46
+ end
47
+ end
48
+
49
+ end
data/test/sanity.rb ADDED
@@ -0,0 +1,22 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ $:.unshift File.expand_path(File.dirname(__FILE__))
3
+
4
+ require 'rubygems'
5
+ require 'simple_oracle_jdbc'
6
+ require 'data_factory'
7
+
8
+ #interface = DataFactory::DBInterface::Oracle.create('sodonnel', 'sodonnel', 'local11gr2')
9
+ interface = SimpleOracleJDBC::Interface.create('sodonnel', 'sodonnel', 'local11gr2.world', 'localhost', '1521')
10
+
11
+ DataFactory::Base.set_database_interface(interface)
12
+
13
+ class Foo < DataFactory::Base
14
+ set_table_name "employees"
15
+ set_column_default "EMP_NAME", 'john'
16
+ end
17
+
18
+
19
+ f = Foo.create!("emp_id" => 1001)
20
+ f.column_values.keys.each do |k|
21
+ puts f.column_values[k]
22
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "."))
3
+
4
+ current_dir = File.expand_path(File.dirname(__FILE__))
5
+
6
+
7
+ # Find all files that end in _test.rb and require them ...
8
+ files = Dir.entries(current_dir).grep(/^[^#].+_test\.rb$/).sort
9
+ files.each do |f|
10
+ require File.join(current_dir, f)
11
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: data_factory
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Stephen O'Donnell
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2013-02-12 00:00:00 +00:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Generates data to insert into database tables, allowing columns to be defaulted or overriden. Intended to be used when testing wide tables where many not null columns may need to be populated but are not part of the test
22
+ email: stephen@betteratoracle.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.md
29
+ files:
30
+ - test/base_api_test.rb
31
+ - test/base_dsl_test.rb
32
+ - test/helper.rb
33
+ - test/sanity.rb
34
+ - test/test_runner.rb
35
+ - lib/data_factory/base.rb
36
+ - lib/data_factory/base_api.rb
37
+ - lib/data_factory/base_dsl.rb
38
+ - lib/data_factory/base_factory.rb
39
+ - lib/data_factory/column.rb
40
+ - lib/data_factory/exceptions.rb
41
+ - lib/data_factory/random.rb
42
+ - lib/data_factory.rb
43
+ - DataFactory-0.1.gem
44
+ - Rakefile.rb
45
+ - README.md
46
+ has_rdoc: true
47
+ homepage: http://betteratoracle.com
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options: []
52
+
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.6
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: A gem to generate template insert statements for use when unit testing database code
76
+ test_files: []
77
+