test: add Visualization Unit Tests#49
Conversation
- Add test-make_bubble_plot.R and test-make_volcano_plot.R - Update test-split_featurePlot.R - Remove source() calls from test-make_volcano_plot.R - Rename avg_log2FC to avg_log_2_fc in make_volcano_plot.R and tests - Hardcode fixed math expectations (log10 transforms) - Regenerate man/make_volcano_plot.Rd Generated using AI
Add missing test helper that defines selectData(), needed by test-make_bubble_plot.R and test-split_featurePlot.R at load time.
…n, seurat_clustering, and cluster_metrics
kelly-sovacool
left a comment
There was a problem hiding this comment.
Significant work is needed to improve these tests. Some patterns I noticed:
- Make sure you're testing the behavior of SCOT functions, not functions from base R or other packages, and not merely the function definitions.
- Make sure each test actually runs the SCOT functions mentioned in the test_that description, or tests the results of a previously-run SCOT function.
- Try to make the tests for functions that return ggplot objects do more than just check that the result is a ggplot. You can test that the various function arguments modify the plots as expected by extracting the plot data, layers, and other plot attributes.
| resolution = 0.8, | ||
| algorithm = 3, | ||
| resolution = resolution, | ||
| algorithm = algorithm, |
There was a problem hiding this comment.
good catch! using parameters is important
| ncl_low <- nlevels(SeuratObject::Idents(low_res)) | ||
| ncl_high <- nlevels(SeuratObject::Idents(high_res)) | ||
|
|
||
| expect_true(ncl_high >= ncl_low) |
There was a problem hiding this comment.
since you set seeds above (good job!), you can use the actual values rather than this conditional
| brca.data = selectData("wu_et_al_BRCA") | ||
| input_cells <- colnames(brca.data) | ||
|
|
||
| set.seed(42) | ||
| brca.clustered = seurat_clustering(so_in = brca.data, npcs_in = 30) |
There was a problem hiding this comment.
follow the tidyverse style guide
| brca.data = selectData("wu_et_al_BRCA") | |
| input_cells <- colnames(brca.data) | |
| set.seed(42) | |
| brca.clustered = seurat_clustering(so_in = brca.data, npcs_in = 30) | |
| brca_data <- selectData("wu_et_al_BRCA") | |
| input_cells <- colnames(brca.data) | |
| set.seed(42) | |
| brca_clustered <- seurat_clustering(so_in = brca.data, npcs_in = 30) |
| test_that("split_featurePlot returns NULL silently with return_list = FALSE", { | ||
| so <- brca_data | ||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = features[1], | ||
| split_ident = "orig.ident", | ||
| plot_image = FALSE, | ||
| return_list = FALSE, | ||
| reduction = "umap" | ||
| ) | ||
| expect_null(res) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot runs when provided with only ncol", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| ncol = 2, | ||
| nrow = NA, | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot works with scalar numeric cutoff values", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| min.cutoff = 0, | ||
| max.cutoff = 5, | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot runs with label = TRUE", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| label = TRUE, | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot works with order = TRUE", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| order = TRUE, | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot works with slot = 'data'", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| slot = "data", | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot works with single split group", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| # Create metadata with single group value | ||
| so$single_group <- "group_1" | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "single_group", | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
|
|
||
| test_that("split_featurePlot works with numeric split_ident values", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| # Create numeric metadata | ||
| so$numeric_group <- sample(1:3, ncol(so), replace = TRUE) | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "numeric_group", | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot handles empty feature list", { | ||
| so <- brca_data | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = character(0), | ||
| split_ident = "orig.ident", | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_equal(length(res), 0) | ||
| }) | ||
|
|
||
| test_that("split_featurePlot with asymmetric cutoff pair", { | ||
| so <- brca_data | ||
| single_feature <- rownames(so@assays$SCT@scale.data)[1] | ||
|
|
||
| res <- split_featurePlot( | ||
| so = so, | ||
| features = single_feature, | ||
| split_ident = "orig.ident", | ||
| min.cutoff = "q10", | ||
| max.cutoff = 3.5, | ||
| plot_image = FALSE, | ||
| return_list = TRUE, | ||
| reduction = "umap" | ||
| ) | ||
|
|
||
| expect_true(is.list(res)) | ||
| expect_true(inherits(res[[single_feature]], "ggplot")) | ||
| }) |
There was a problem hiding this comment.
perhaps you could add expectations that do more than only check that the function was able to successfully return lists of ggplots, and actually test that the different function arguments you provide have an effect on the ggplot objects themselves, e.g. testing the layers, labels, plot data, etc;
There was a problem hiding this comment.
added some additional expectations for checking layers and plot data
| @@ -15,6 +15,25 @@ | |||
| #' @return The updated Seurat single cell object with recalculated PCA, | |||
| #' neighbors, UMAP and clusters | |||
| seurat_clustering <- function(so_in, npcs_in, resolution = 0.8, algorithm = 3) { | |||
There was a problem hiding this comment.
I think we should consider renaming this function to a verb. see https://style.tidyverse.org/functions.html @wong-nw
Co-authored-by: Kelly Sovacool, PhD <kelly-sovacool@users.noreply.github.com>
Changes
make_volcano_plot.R
Refactored volcano logic to use snake_case (avg_log_2_fc), aligned significance labels, and ensured consistent internal data-frame column naming.
Regenerated docs to match the updated argument/field naming in the R function.
Added comprehensive volcano tests covering object creation, transformations, significance logic, edge cases, and missing-column validation.
split_featurePlot.R
Added/updated split feature plot tests for layout behavior, return types, parameter options, and invalid-input handling.
make_bubble_plot.R
Added unit tests for bubble plot creation, structure checks, identity handling, scaling behavior, and expected error cases.
Issues
issue 47 - see checklist
PR Checklist
(
Strikethroughany points that are not applicable.)[ ] Update the docs if there are any API changes (roxygen2 comments, vignettes, readme, etc.).[ ] UpdateNEWS.mdwith a short description of any user-facing changes and reference the PR number. Follow the style described in https://style.tidyverse.org/news.htmldevtools::check()locally and fix all notes, warnings, and errors.R-CMD-checksucceeds on the most recent user commit.