on main help sign in
Add test framework

This reduces the amount of boilerplate required to run a test and also has the added benefit of extending tests as we can now easily test both the completion and abort paths. So far two tests have been ported over, create_table and add_column, with the rest to follow. The framework also improves the output of tests, adding sections clearly indicating in which part of the migration process a test failed.

commit945e6374d87c729fd2b3302142f6a64f0541174b authorfabianlindfors <fabian@flapplabs.se> dateFri 14 Jan 2022, 23:49 (5 years ago) parent0e0c24b
3 files changed+396−200
tests/add_column.rs+93−96 tests/common.rs+198−1 tests/create_table.rs+105−103
tests/add_column.rs+93−96
@@ -4,76 +4,59 @@ mod common;
44
55#[test]
66fn add_column() {
7− let (mut reshape, mut old_db, mut new_db) = common::setup();
8−
9− let create_users_table = Migration::new("create_user_table", None).with_action(
10− CreateTableBuilder::default()
11− .name("users")
12− .primary_key(vec!["id".to_string()])
13− .columns(vec![
14− ColumnBuilder::default()
15− .name("id")
16− .data_type("INTEGER")
17− .build()
18− .unwrap(),
19− ColumnBuilder::default()
20− .name("name")
21− .data_type("TEXT")
22− .build()
23− .unwrap(),
24− ])
25− .build()
26− .unwrap(),
7+ let mut test = common::Test::new("Add column");
8+
9+ test.first_migration(
10+ Migration::new("create_user_table", None).with_action(
11+ CreateTableBuilder::default()
12+ .name("users")
13+ .primary_key(vec!["id".to_string()])
14+ .columns(vec![
15+ ColumnBuilder::default()
16+ .name("id")
17+ .data_type("INTEGER")
18+ .build()
19+ .unwrap(),
20+ ColumnBuilder::default()
21+ .name("name")
22+ .data_type("TEXT")
23+ .build()
24+ .unwrap(),
25+ ])
26+ .build()
27+ .unwrap(),
28+ ),
2729 );
2830
29− let add_first_last_name_columns = Migration::new("add_first_and_last_name_columns", None)
30− .with_action(AddColumn {
31− table: "users".to_string(),
32− column: Column {
33− name: "first".to_string(),
34− data_type: "TEXT".to_string(),
35− nullable: false,
36− default: None,
37− generated: None,
38− },
39− up: Some("(STRING_TO_ARRAY(name, ' '))[1]".to_string()),
40− })
41− .with_action(AddColumn {
42− table: "users".to_string(),
43− column: Column {
44− name: "last".to_string(),
45− data_type: "TEXT".to_string(),
46− nullable: false,
47− default: None,
48− generated: None,
49− },
50− up: Some("(STRING_TO_ARRAY(name, ' '))[2]".to_string()),
51− });
52−
53− let first_migrations = vec![create_users_table.clone()];
54− let second_migrations = vec![
55− create_users_table.clone(),
56− add_first_last_name_columns.clone(),
57− ];
58−
59− // Run first migration, should automatically finish
60− reshape.migrate(first_migrations.clone()).unwrap();
61−
62− // Update search paths
63− old_db
64− .simple_query(&reshape::schema_query_for_migration(
65− &first_migrations.last().unwrap().name,
66− ))
67− .unwrap();
68− new_db
69− .simple_query(&reshape::schema_query_for_migration(
70− &first_migrations.last().unwrap().name,
71− ))
72− .unwrap();
31+ test.second_migration(
32+ Migration::new("add_first_and_last_name_columns", None)
33+ .with_action(AddColumn {
34+ table: "users".to_string(),
35+ column: Column {
36+ name: "first".to_string(),
37+ data_type: "TEXT".to_string(),
38+ nullable: false,
39+ default: None,
40+ generated: None,
41+ },
42+ up: Some("(STRING_TO_ARRAY(name, ' '))[1]".to_string()),
43+ })
44+ .with_action(AddColumn {
45+ table: "users".to_string(),
46+ column: Column {
47+ name: "last".to_string(),
48+ data_type: "TEXT".to_string(),
49+ nullable: false,
50+ default: None,
51+ generated: None,
52+ },
53+ up: Some("(STRING_TO_ARRAY(name, ' '))[2]".to_string()),
54+ }),
55+ );
7356
74− // Insert some test users
75− new_db
76− .simple_query(
57+ test.after_first(|db| {
58+ // Insert some test users
59+ db.simple_query(
7760 "
7861 INSERT INTO users (id, name) VALUES
7962 (1, 'John Doe'),
@@ -81,39 +64,53 @@ fn add_column() {
8164 ",
8265 )
8366 .unwrap();
67+ });
8468
85− // Run second migration
86− reshape.migrate(second_migrations.clone()).unwrap();
87− new_db
88− .simple_query(&reshape::schema_query_for_migration(
89− &second_migrations.last().unwrap().name,
90− ))
91− .unwrap();
69+ test.intermediate(|old_db, new_db| {
70+ // Check that the existing users have the new columns populated
71+ let expected = vec![("John", "Doe"), ("Jane", "Doe")];
72+ assert!(new_db
73+ .query("SELECT first, last FROM users ORDER BY id", &[],)
74+ .unwrap()
75+ .iter()
76+ .map(|row| (row.get("first"), row.get("last")))
77+ .eq(expected));
78+
79+ // Insert data using old schema and make sure the new columns are populated
80+ old_db
81+ .simple_query("INSERT INTO users (id, name) VALUES (3, 'Test Testsson')")
82+ .unwrap();
83+ let (first_name, last_name): (String, String) = new_db
84+ .query_one("SELECT first, last from users WHERE id = 3", &[])
85+ .map(|row| (row.get("first"), row.get("last")))
86+ .unwrap();
87+ assert_eq!(
88+ ("Test", "Testsson"),
89+ (first_name.as_ref(), last_name.as_ref())
90+ );
91+ });
9292
93− // Check that the existing users have the new columns populated
94− let expected = vec![("John", "Doe"), ("Jane", "Doe")];
95− assert!(new_db
96− .query("SELECT first, last FROM users ORDER BY id", &[],)
97− .unwrap()
98− .iter()
99− .map(|row| (row.get("first"), row.get("last")))
100− .eq(expected));
93+ test.after_completion(|db| {
94+ let expected = vec![("John", "Doe"), ("Jane", "Doe"), ("Test", "Testsson")];
95+ assert!(db
96+ .query("SELECT first, last FROM users ORDER BY id", &[],)
97+ .unwrap()
98+ .iter()
99+ .map(|row| (row.get("first"), row.get("last")))
100+ .eq(expected));
101+ });
101102
102− // Insert data using old schema and make sure the new columns are populated
103− old_db
104− .simple_query("INSERT INTO users (id, name) VALUES (3, 'Test Testsson')")
105− .unwrap();
106− let (first_name, last_name): (String, String) = new_db
107− .query_one("SELECT first, last from users WHERE id = 3", &[])
108− .map(|row| (row.get("first"), row.get("last")))
109− .unwrap();
110− assert_eq!(
111− ("Test", "Testsson"),
112− (first_name.as_ref(), last_name.as_ref())
113− );
103+ test.after_abort(|db| {
104+ let expected = vec![("John Doe"), ("Jane Doe"), ("Test Testsson")];
105+ assert!(db
106+ .query("SELECT name FROM users ORDER BY id", &[],)
107+ .unwrap()
108+ .iter()
109+ .map(|row| row.get::<'_, _, String>("name"))
110+ .eq(expected));
111+ });
114112
115− reshape.complete().unwrap();
116− common::assert_cleaned_up(&mut new_db);
113+ test.run()
117114}
118115
119116#[test]
tests/common.rs+198−1
@@ -1,5 +1,202 @@
1+use colored::Colorize;
12use postgres::{Client, NoTls};
2−use reshape::Reshape;
3+use reshape::{migrations::Migration, Reshape};
4+
5+pub struct Test<'a> {
6+ name: &'a str,
7+ reshape: Reshape,
8+ old_db: Client,
9+ new_db: Client,
10+
11+ first_migration: Option<Migration>,
12+ second_migration: Option<Migration>,
13+
14+ after_first_fn: Option<fn(&mut Client) -> ()>,
15+ intermediate_fn: Option<fn(&mut Client, &mut Client) -> ()>,
16+ after_completion_fn: Option<fn(&mut Client) -> ()>,
17+ after_abort_fn: Option<fn(&mut Client) -> ()>,
18+}
19+
20+impl Test<'_> {
21+ pub fn new<'a>(name: &'a str) -> Test<'a> {
22+ let connection_string = std::env::var("POSTGRES_CONNECTION_STRING")
23+ .unwrap_or("postgres://postgres:postgres@localhost/reshape_test".to_string());
24+
25+ let old_db = Client::connect(&connection_string, NoTls).unwrap();
26+ let new_db = Client::connect(&connection_string, NoTls).unwrap();
27+
28+ let reshape = Reshape::new(&connection_string).unwrap();
29+
30+ Test {
31+ name,
32+ reshape,
33+ old_db,
34+ new_db,
35+ first_migration: None,
36+ second_migration: None,
37+ after_first_fn: None,
38+ intermediate_fn: None,
39+ after_completion_fn: None,
40+ after_abort_fn: None,
41+ }
42+ }
43+
44+ pub fn first_migration(&mut self, migration: Migration) -> &mut Self {
45+ self.first_migration = Some(migration);
46+ self
47+ }
48+
49+ pub fn second_migration(&mut self, migration: Migration) -> &mut Self {
50+ self.second_migration = Some(migration);
51+ self
52+ }
53+
54+ pub fn after_first(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
55+ self.after_first_fn = Some(f);
56+ self
57+ }
58+
59+ pub fn intermediate(&mut self, f: fn(&mut Client, &mut Client) -> ()) -> &mut Self {
60+ self.intermediate_fn = Some(f);
61+ self
62+ }
63+
64+ pub fn after_completion(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
65+ self.after_completion_fn = Some(f);
66+ self
67+ }
68+
69+ pub fn after_abort(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
70+ self.after_abort_fn = Some(f);
71+ self
72+ }
73+}
74+
75+enum RunType {
76+ Simple,
77+ Completion,
78+ Abort,
79+}
80+
81+impl Test<'_> {
82+ pub fn run(&mut self) {
83+ if self.second_migration.is_some() {
84+ // Run to completion
85+ print_heading(&format!("Test completion: {}", self.name));
86+ self.run_internal(RunType::Completion);
87+
88+ // Run and abort
89+ print_heading(&format!("Test abort: {}", self.name));
90+ self.run_internal(RunType::Abort);
91+ } else {
92+ print_heading(&format!("Test: {}", self.name));
93+ self.run_internal(RunType::Simple);
94+ }
95+ }
96+
97+ fn run_internal(&mut self, run_type: RunType) {
98+ print_subheading("Clearing database");
99+ self.reshape.remove().unwrap();
100+
101+ // Apply first migration, will automatically complete
102+ print_subheading("Applying first migration");
103+ let first_migration = self
104+ .first_migration
105+ .as_ref()
106+ .expect("no starting migration set");
107+ self.reshape.migrate(vec![first_migration.clone()]).unwrap();
108+
109+ // Update search path
110+ self.old_db
111+ .simple_query(&reshape::schema_query_for_migration(&first_migration.name))
112+ .unwrap();
113+
114+ // Run setup function
115+ if let Some(after_first_fn) = self.after_first_fn {
116+ print_subheading("Running setup and first checks");
117+ after_first_fn(&mut self.old_db);
118+ print_success();
119+ }
120+
121+ // Apply second migration
122+ if let Some(second_migration) = &self.second_migration {
123+ print_subheading("Applying second migration");
124+ self.reshape
125+ .migrate(vec![first_migration.clone(), second_migration.clone()])
126+ .unwrap();
127+
128+ // Update search path
129+ self.new_db
130+ .simple_query(&reshape::schema_query_for_migration(&second_migration.name))
131+ .unwrap();
132+
133+ if let Some(intermediate_fn) = self.intermediate_fn {
134+ print_subheading("Running intermediate checks");
135+ intermediate_fn(&mut self.old_db, &mut self.new_db);
136+ print_success();
137+ }
138+
139+ match run_type {
140+ RunType::Completion => {
141+ print_subheading("Completing");
142+ self.reshape.complete().unwrap();
143+
144+ if let Some(after_completion_fn) = self.after_completion_fn {
145+ print_subheading("Running post-completion checks");
146+ after_completion_fn(&mut self.new_db);
147+ print_success();
148+ }
149+ }
150+ RunType::Abort => {
151+ print_subheading("Aborting");
152+ self.reshape.abort().unwrap();
153+
154+ if let Some(after_abort_fn) = self.after_abort_fn {
155+ print_subheading("Running post-abort checks");
156+ after_abort_fn(&mut self.old_db);
157+ print_success();
158+ }
159+ }
160+ _ => {}
161+ }
162+ }
163+
164+ print_subheading("Checking cleanup");
165+ assert_cleaned_up(&mut self.new_db);
166+ print_success();
167+ }
168+}
169+
170+fn print_heading(text: &str) {
171+ let delimiter = std::iter::repeat("=").take(50).collect::<String>();
172+
173+ println!();
174+ println!();
175+ println!("{}", delimiter.blue().bold());
176+ println!("{}", add_spacer(text, "=").blue().bold());
177+ println!("{}", delimiter.blue().bold());
178+}
179+
180+fn print_subheading(text: &str) {
181+ println!();
182+ println!("{}", add_spacer(text, "=").blue());
183+}
184+
185+fn print_success() {
186+ println!("{}", add_spacer("Success", "=").green());
187+}
188+
189+fn add_spacer(text: &str, char: &str) -> String {
190+ const TARGET_WIDTH: usize = 50;
191+ let num_of_chars = (TARGET_WIDTH - text.len() - 2) / 2;
192+ let spacer = std::iter::repeat(char)
193+ .take(num_of_chars)
194+ .collect::<String>();
195+
196+ let extra = if text.len() % 2 == 0 { "" } else { char };
197+
198+ format!("{spacer} {text} {spacer}{extra}", spacer = spacer)
199+}
3200
4201pub fn setup() -> (Reshape, Client, Client) {
5202 let connection_string = std::env::var("POSTGRES_CONNECTION_STRING")
tests/create_table.rs+105−103
@@ -4,113 +4,115 @@ mod common;
44
55#[test]
66fn create_table() {
7− let (mut reshape, mut db, _) = common::setup();
8−
9− let create_table_migration = Migration::new("create_users_table", None).with_action(
10− CreateTableBuilder::default()
11− .name("users")
12− .primary_key(vec!["id".to_string()])
13− .columns(vec![
14− ColumnBuilder::default()
15− .name("id")
16− .data_type("INTEGER")
17− .generated("ALWAYS AS IDENTITY")
18− .build()
19− .unwrap(),
20− ColumnBuilder::default()
21− .name("name")
22− .data_type("TEXT")
23− .build()
24− .unwrap(),
25− ColumnBuilder::default()
26− .name("created_at")
27− .data_type("TIMESTAMP")
28− .nullable(false)
29− .default_value("NOW()")
30− .build()
31− .unwrap(),
32− ])
33− .build()
34− .unwrap(),
7+ let mut test = common::Test::new("Create table");
8+
9+ test.first_migration(
10+ Migration::new("create_users_table", None).with_action(
11+ CreateTableBuilder::default()
12+ .name("users")
13+ .primary_key(vec!["id".to_string()])
14+ .columns(vec![
15+ ColumnBuilder::default()
16+ .name("id")
17+ .data_type("INTEGER")
18+ .generated("ALWAYS AS IDENTITY")
19+ .build()
20+ .unwrap(),
21+ ColumnBuilder::default()
22+ .name("name")
23+ .data_type("TEXT")
24+ .build()
25+ .unwrap(),
26+ ColumnBuilder::default()
27+ .name("created_at")
28+ .data_type("TIMESTAMP")
29+ .nullable(false)
30+ .default_value("NOW()")
31+ .build()
32+ .unwrap(),
33+ ])
34+ .build()
35+ .unwrap(),
36+ ),
3537 );
3638
37− reshape
38− .migrate(vec![create_table_migration.clone()])
39− .unwrap();
40−
41− // Ensure table was created
42− let result = db
43− .query_opt(
44− "
45− SELECT table_name
46− FROM information_schema.tables
47− WHERE table_name = 'users' AND table_schema = 'public'",
48− &[],
49− )
50− .unwrap();
51− assert!(result.is_some());
52−
53− // Ensure table has the right columns
54− let result = db
55− .query(
56− "
57− SELECT column_name, column_default, is_nullable, data_type
58− FROM information_schema.columns
59− WHERE table_name = 'users' AND table_schema = 'public'
60− ORDER BY ordinal_position",
61− &[],
62− )
63− .unwrap();
64−
65− // id column
66− let id_row = &result[0];
67− assert_eq!("id", id_row.get::<_, String>("column_name"));
68− assert!(id_row.get::<_, Option<String>>("column_default").is_none());
69− assert_eq!("NO", id_row.get::<_, String>("is_nullable"));
70− assert_eq!("integer", id_row.get::<_, String>("data_type"));
71−
72− // name column
73− let name_row = &result[1];
74− assert_eq!("name", name_row.get::<_, String>("column_name"));
75− assert!(name_row
76− .get::<_, Option<String>>("column_default")
77− .is_none());
78− assert_eq!("YES", name_row.get::<_, String>("is_nullable"));
79− assert_eq!("text", name_row.get::<_, String>("data_type"));
80−
81− // created_at column
82− let created_at_column = &result[2];
83− assert_eq!(
84− "created_at",
85− created_at_column.get::<_, String>("column_name")
86− );
87− assert!(created_at_column
88− .get::<_, Option<String>>("column_default")
89− .is_some());
90− assert_eq!("NO", created_at_column.get::<_, String>("is_nullable"));
91− assert_eq!(
92− "timestamp without time zone",
93− created_at_column.get::<_, String>("data_type")
94− );
39+ test.after_first(|db| {
40+ // Ensure table was created
41+ let result = db
42+ .query_opt(
43+ "
44+ SELECT table_name
45+ FROM information_schema.tables
46+ WHERE table_name = 'users' AND table_schema = 'public'",
47+ &[],
48+ )
49+ .unwrap();
50+ assert!(result.is_some());
51+
52+ // Ensure table has the right columns
53+ let result = db
54+ .query(
55+ "
56+ SELECT column_name, column_default, is_nullable, data_type
57+ FROM information_schema.columns
58+ WHERE table_name = 'users' AND table_schema = 'public'
59+ ORDER BY ordinal_position",
60+ &[],
61+ )
62+ .unwrap();
63+
64+ // id column
65+ let id_row = &result[0];
66+ assert_eq!("id", id_row.get::<_, String>("column_name"));
67+ assert!(id_row.get::<_, Option<String>>("column_default").is_none());
68+ assert_eq!("NO", id_row.get::<_, String>("is_nullable"));
69+ assert_eq!("integer", id_row.get::<_, String>("data_type"));
70+
71+ // name column
72+ let name_row = &result[1];
73+ assert_eq!("name", name_row.get::<_, String>("column_name"));
74+ assert!(name_row
75+ .get::<_, Option<String>>("column_default")
76+ .is_none());
77+ assert_eq!("YES", name_row.get::<_, String>("is_nullable"));
78+ assert_eq!("text", name_row.get::<_, String>("data_type"));
79+
80+ // created_at column
81+ let created_at_column = &result[2];
82+ assert_eq!(
83+ "created_at",
84+ created_at_column.get::<_, String>("column_name")
85+ );
86+ assert!(created_at_column
87+ .get::<_, Option<String>>("column_default")
88+ .is_some());
89+ assert_eq!("NO", created_at_column.get::<_, String>("is_nullable"));
90+ assert_eq!(
91+ "timestamp without time zone",
92+ created_at_column.get::<_, String>("data_type")
93+ );
94+
95+ // Ensure the primary key has the right columns
96+ let primary_key_columns: Vec<String> = db
97+ .query(
98+ "
99+ SELECT a.attname AS column
100+ FROM pg_index i
101+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
102+ JOIN pg_class t ON t.oid = i.indrelid
103+ WHERE t.relname = 'users' AND i.indisprimary
104+ ",
105+ &[],
106+ )
107+ .unwrap()
108+ .iter()
109+ .map(|row| row.get("column"))
110+ .collect();
95111
96− // Ensure the primary key has the right columns
97− let primary_key_columns: Vec<String> = db
98− .query(
99− "
100− SELECT a.attname AS column
101− FROM pg_index i
102− JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
103− WHERE i.indrelid = 'users'::regclass AND i.indisprimary
104− ",
105− &[],
106− )
107− .unwrap()
108− .iter()
109− .map(|row| row.get("column"))
110− .collect();
112+ assert_eq!(vec!["id"], primary_key_columns);
113+ });
111114
112− assert_eq!(vec!["id"], primary_key_columns);
113− common::assert_cleaned_up(&mut db);
115+ test.run();
114116}
115117
116118#[test]