How to upgrade from flyway 3 directly to flyway 5 - flyway

Working on a product that is deployed by many clients in many production environments. It includes at least one Spring Boot app.
We've used flyway for db schema migrations. Upgrading from Spring Boot 1.5.x to 2.0.x has bumped our flyway version from 3.x to 5.x.
The Spring Boot migration guide simply says to upgrade to flyway 4 before the boot upgrade. However, this would require all of our customers to do an intermediate upgrade before being able to upgrade to the latest.
So, the question is: How would you upgrade from flyway 3 directly to flyway 5?

Step 0.
Upgrade to spring boot v2.1 (and therby implicitly to flyway 5).
Step 1.
Since schema_version was used in flyway 3.x let new flyway versions know that they should keep using this table.:
# application.yml
spring.flyway.table: schema_version # prior flyway version used this table and we keep it
Step 2.
Create file src/main/ressources/db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql for upgrading the meta table based on the dialect you use.
See https://github.com/flyway/flyway/commit/cea8526d7d0a9b0ec35bffa5cb43ae08ea5849e4#diff-b9cb194749ffef15acc9969b90488d98 for the update scripts of several dialects.
Here is the one for postgres and assuming the flyway table name is schema_version:
-- src/main/ressources/db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql
DROP INDEX "schema_version_vr_idx";
DROP INDEX "schema_version_ir_idx";
ALTER TABLE "schema_version" DROP COLUMN "version_rank";
ALTER TABLE "schema_version" DROP CONSTRAINT "schema_version_pk";
ALTER TABLE "schema_version" ALTER COLUMN "version" DROP NOT NULL;
ALTER TABLE "schema_version" ADD CONSTRAINT "schema_version_pk" PRIMARY KEY ("installed_rank");
UPDATE "schema_version" SET "type"='BASELINE' WHERE "type"='INIT';
Step 3.
Create Java file your.package/FlywayUpdate3To4Callback.java
Please note that this does the following:
Run the sql script from Step 2
call Flyway.repair()
// FlywayUpdate3To4Callback.java
package your.package;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback implements Callback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
private boolean checkColumnExists(Configuration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
#Override
public boolean supports(Event event, Context context) {
return event == Event.BEFORE_VALIDATE;
}
#Override
public boolean canHandleInTransaction(Event event, Context context) {
return false;
}
#Override
public void handle(Event event, Context context) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(context.getConfiguration());
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("db/migration/common/flyway_upgradeMetaDataTable_V3_to_V4.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(context.getConnection(), resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
}
Step 4.
Run spring boot.
The log should show info messages similar to these:
...FlywayUpdate3To4Callback : Upgrading metadata table the Flyway 4.0 format
...FlywayUpdate3To4Callback : Flyway metadata table updated successfully.
Credits
This answer is based on Eduardo Rodrigues answer by changing:
Use Event.BEFORE_VALIDATE to trigger a flyway callback that upgrade flyway 3 to 4.
more information on application.yml setup
provide upgrade sql migration script

In case I'm not the last person on the planet to still be upgrading from 3 to 5.
Problem:
I wanted the upgrade to be transparent to other developers on the project as well as not requiring any special deployment instructions when upgrading on the live applications, so I did the following.
I had a look at how version 4 handled the upgrade:
In Flyway.java a call is made to MetaDataTableImpl.upgradeIfNecessary
upgradeIfNecessary checks if the version_rank column still exists, and if so runs a migration script called upgradeMetaDataTable.sql from org/flywaydb/core/internal/dbsupport/YOUR_DB/
If upgradeIfNecessary executed, then Flyway.java runs a DbRepair calling repairChecksumsAndDescriptions
This is easy enough to do manually but to make it transparent. The app is a spring app, but not a spring boot app, so at the time I had flyway running migrations automatically on application startup by having LocalContainerEntityManager bean construction dependent on the flyway bean, which would call migrate as its init method (explained here Flyway Spring JPA2 integration - possible to keep schema validation?), so the order of bootstrapping would be:
Flyway bean created -> Flyway migrate called -> LocalContainerEntityManager created
Solution:
I changed the order of bootstrapping to:
Flyway bean created -> Flyway3To4Migrator -> LocalContainerEntityManager created
where Flyway3To4Migrator would perform the schema_table changes if needed, run the repair if the upgrade happened, and then always run flyway.migrate to continue the migrations.
#Configuration
public class AppConfiguration {
#Bean
// Previously: #DependsOn("flyway")
#DependsOn("flyway3To4Migrator")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
...
}
// Previously: #Bean(initMethod = "migrate")
#Bean
public Flyway flyway(DataSource dataSource) {
...
}
}
#Component
#DependsOn("flyway")
public class Flyway3To4Migrator {
private final Log logger = LogFactory.getLog(getClass());
private Flyway flyway;
#Autowired
public Flyway3To4Migrator(Flyway flyway) {
this.flyway = flyway;
}
#PostConstruct
public void migrate() throws SQLException, MetaDataAccessException {
DataSource dataSource = flyway.getDataSource();
boolean versionRankColumnExists = checkColumnExists(dataSource);
if (versionRankColumnExists) {
logger.info("Upgrading metadata table to the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("upgradeMetaDataTable.sql", getClass().getClassLoader());
ScriptUtils.executeSqlScript(dataSource.getConnection(), resource);
logger.info("Metadata table successfully upgraded to the Flyway 4.0 format.");
logger.info("Running flyway:repair for Flyway upgrade.");
flyway.repair();
logger.info("Complete flyway:repair.");
}
logger.info("Continuing with normal Flyway migration.");
flyway.migrate();
}
private boolean checkColumnExists(DataSource dataSource) throws MetaDataAccessException {
return (Boolean) JdbcUtils.extractDatabaseMetaData(
dataSource, dbmd -> {
ResultSet rs = dbmd.getColumns(
null, null,
"schema_version",
"version_rank");
return rs.next();
});
}
}
A few things to note:
At some point we will remove the extra Flyway3To4Migrator class and revert the configuration to the way it was.
I copied the relevant upgradeMetaDataTable.sql file for my database from the v4 Flyway jar and simplified it to my table names etc. You could grab the schema and table names from flyway if you needed to.
there is no transaction management around the SQL script, you might want to add that
Flyway3To4Migrator calls flyway.repair(), which does a little more than DbRepair.repairChecksumsAndDescriptions(), but we were happy to accept the database must be in a good state before its run

If you're using Spring Boot, you can register a callback that does the upgrade on beforeMigrate(). The code is similar to #trf and looks like this:
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback extends BaseFlywayCallback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
#Override
public void beforeMigrate(Connection connection) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(flywayConfiguration);
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("upgradeMetaDataTable.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(connection, resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
private boolean checkColumnExists(FlywayConfiguration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
Notice that you don't need to manually call flyway.migrate() here.

The code above is not compatible with version 5. It uses deprecated classes.
Here is an updated version.
import lombok.extern.slf4j.Slf4j;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.callback.Callback;
import org.flywaydb.core.api.callback.Context;
import org.flywaydb.core.api.callback.Event;
import org.flywaydb.core.api.configuration.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.stereotype.Component;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
#Component
#Order(HIGHEST_PRECEDENCE)
#Slf4j
public class FlywayUpdate3To4Callback implements Callback {
private final Flyway flyway;
public FlywayUpdate3To4Callback(#Lazy Flyway flyway) {
this.flyway = flyway;
}
private boolean checkColumnExists(Configuration flywayConfiguration) throws MetaDataAccessException {
return (boolean) JdbcUtils.extractDatabaseMetaData(flywayConfiguration.getDataSource(),
callback -> callback
.getColumns(null, null, flywayConfiguration.getTable(), "version_rank")
.next());
}
#Override
public boolean supports(Event event, Context context) {
return event == Event.BEFORE_VALIDATE;
}
#Override
public boolean canHandleInTransaction(Event event, Context context) {
return false;
}
#Override
public void handle(Event event, Context context) {
boolean versionRankColumnExists = false;
try {
versionRankColumnExists = checkColumnExists(context.getConfiguration());
} catch (MetaDataAccessException e) {
log.error("Cannot obtain flyway metadata");
return;
}
if (versionRankColumnExists) {
log.info("Upgrading metadata table the Flyway 4.0 format ...");
Resource resource = new ClassPathResource("db/migration/flyway_upgradeMetaDataTable_V3_to_V4.sql",
Thread.currentThread().getContextClassLoader());
ScriptUtils.executeSqlScript(context.getConnection(), resource);
log.info("Flyway metadata table updated successfully.");
// recalculate checksums
flyway.repair();
}
}
}

I tried to skip v4 too but didn't work. Running the repair from 3 to 5 will make the checksums correct, but won't change the schema_version format. That has changed as well.
It seems you need to go to v4 first. Even if temporarily just to run mvn flyway:validate, which will repair schema_version.
I've done this on this repo: https://github.com/fabiofalci/flyway-from-3-to-5/commits/5.0.7
The first commit is v3, the second commit is v4 (where I ran validate) and then the third commit on v5 the schema is correct.

It worked for me, except I had to put Event.BEFORE_VALIDATE again instead of Event.BEFORE_MIGRATE that was present in the last version of the FlywayUpdate3To4Callback Class. This is because I had a checksum not valid on a migration already run, so it needed to be fixed before validating and not before migrating. Thanks.

Related

Xamarin.Forms and EntityFrameworkCore 3.x how to properly create a database and apply migrations?

what is the proper way to create an EF Core database on an Android/iOS device with Xamarin.Forms and EntityFrameworkCore?
I've come across the EnsureCreated vs Migrate thing and since I'm planning on changing the database structure in the future, I'd like to be able to apply those migrations to an existing database, thus I've chosen the Migrate approach.
However, when I call the Migrate method, it doesn't seem to create a database. I've managed to make it work in case I copy the pre-generated database file on the devices beforehand, but that's not what I want. Is there a way to tell EF Core to check if there's an existing database, create a new database if not, and then apply pending migrations to it?
Here's what I've done so far:
I've created a .net standard class library "Data" that will hold all my database classes (context, models, and migrations).
I've changed the TargetFramework of the "Data" project from <TargetFramework>netstandard2.0</TargetFramework> to <TargetFrameworks>netcoreapp2.0;netstandard2.0</TargetFrameworks> so I could run PMC commands on it.
I've installed these nuget packages to the "Data" project:
EntityFrameworkCore
EntityFrameworkCore.Sqlite
EntityFrameworkCore.Tools
EntityFrameworkCore.Design
I've created a database context class. I've made two constructors for it. The default one will only be used by the PMC commands for generating migrations. The other one will be used in production.
public class MyTestContext : DbContext
{
private string _databasePath;
public DbSet<Issue> Issues { get; set; }
[Obsolete("Don't use this for production. This is only for creating migrations.")]
public MyTestContext() : this("nothing.db")
{
}
public MyTestContext(string databasePath)
{
_databasePath = databasePath;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//I've also tried Filename={_databasePath} here, didn't work either
optionsBuilder.UseSqlite($"Data Source={_databasePath}");
}
}
I've created a test model class, just to have something to play with
public class Issue
{
public string Id { get; set; }
}
I created an inital migration using this command, which completed successfully:
Add-Migration Migration001 -Context MyTestContext -Output Migrations
I added a call to the Migrate method in the App.xaml.cs of my Xamarin.Forms project to test if it works.
public partial class App : Application{
//... other code
protected override async void OnStart()
{
base.OnStart();
using (var db = new MyTestContext(_dbPath))
{
try
{
db.Database.Migrate();
db.Issues.Add(new Issue
{
Id = "TestIssueId"
});
db.SaveChanges();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
//... other code
}
The _dbPath variable contains this (on Android where I'm testing it):
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "test.db");
After all this, I'm getting this exception:
Microsoft.Data.Sqlite.SqliteException: SQLite Error 1: 'no such table: Issues'.

After accidentally deleting Data Dictionary (app:dictionary) Alfresco doesn't start

One operator deleted Data Dictionary and restarted Alfresco 3.4.12 Enterprise Edition. The context /alfresco doesn't start with the following exception:
17:43:11,100 INFO [STDOUT] 17:43:11,097 ERROR [web.context.ContextLoader] Context initialization failed
org.alfresco.error.AlfrescoRuntimeException: 08050000 Failed to find 'app:dictionary' node
at org.alfresco.repo.action.scheduled.ScheduledPersistedActionServiceImpl.locatePersistanceFolder(ScheduledPersistedActionServiceImpl.java:132)
Looking at the source code in org.alfresco.repo.action.scheduled.ScheduledPersistedActionServiceImpl.java, the path is hardwired.
Then we followed the tip from https://community.alfresco.com/thread/202859-error-failed-to-find-appdictionary-node, editing bootstrap-context.xml, comment out the class.
After the change the error went over, now the RenditionService couldn't start.
We're looking for a way to recover the deleted node, since we can obtain the nodeid from the database. So we created a small class and invoke it through spring in bootstrap-context.xml, but it's failing due to permissions. Could you take a look at the code and tell us what's wrong. The code is:
package com.impulseit.test;
import javax.transaction.UserTransaction;
import org.alfresco.repo.node.archive.NodeArchiveService;
import org.alfresco.repo.node.archive.RestoreNodeReport;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.repository.NodeRef;
public class RestoreNode {
private NodeArchiveService nodeArchiveService;
private ServiceRegistry serviceRegistry;
private String nodeName ="archive://SpacesStore/adfc0cfe-e20b-467f-ad71-253aea8f9ac9";
public void setNodeArchiveService(NodeArchiveService value)
{
this.nodeArchiveService = value;
}
public void setServiceRegistry(ServiceRegistry value)
{
this.serviceRegistry = value;
}
public void doRestore() {
RunAsWork<Void> runAsWork = new RunAsWork<Void>()
{
public Void doWork() throws Exception
{
NodeRef nodeRef = new NodeRef(nodeName);
//RestoreNodeReport restoreNodeReport =
UserTransaction trx_A = serviceRegistry.getTransactionService().getUserTransaction();
trx_A.begin();
AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName());
RestoreNodeReport restored = nodeArchiveService.restoreArchivedNode(nodeRef);
trx_A.commit();
return null;
}
};
AuthenticationUtil.runAs(runAsWork,AuthenticationUtil.getSystemUserName());
}
public RestoreNode() {
}
}
The exception is:
19:31:21,747 User:admin ERROR [node.archive.NodeArchiveServiceImpl] An unhandled exception stopped the restore
java.lang.NullPointerException
at org.alfresco.repo.security.permissions.impl.model.PermissionModel.getPermissionReference(PermissionModel.java:1315)
at org.alfresco.repo.security.permissions.impl.PermissionServiceImpl.getPermissionReference(PermissionServiceImpl.java:956)
at org.alfresco.repo.security.permissions.impl.PermissionServiceImpl.hasPermission(PermissionServiceImpl.java:976)
Thank you in advance.
Luis

Unit Testing code built with MVVMCross SQLite Plugin

I'm currently building a Xamarin.Forms project using MVVMCross. In order to test my platform specific code I am using Nunit.Xamarin which features an app that run tests on device.
This test app is a forms app but doesn't use MVVMCross and I haven't had any luck setting it up to use MVVMCross due to the fact the Application class loads an App of type NUnit.Runner.App whereas MVVMCross requires MvxFormsApp.
I want to test this class the saves and loads user data from an SQLite Database:
public class DataStorageService : IDataStorageService
{
private readonly SQLiteConnection _connection;
public User UserData
{
get { return _connection.Table<User>().FirstOrDefault(); }
set { _connection.InsertOrReplace(value); }
}
public DataStorageService(IMvxSqliteConnectionFactory factory)
{
_connection = factory.GetConnection(DataStorageConstants.LocalDatabaseName);
_connection.CreateTable<User>();
}
}
I want to actually test that it saves and loads from a local SQLite database so I don't want to mock the IMvxSqliteConnectionFactory. I tried installing MVVMCross and the SQLite plugin into the project and then passing in the Android implementation of the connection factory but that repeatedly threw a typeloadexception.
Any ideas as to how I can set up this test with MVVMCross (or are there alternatives?) and dependency injection?
It is possible :) The important stuff happens in the MvxSplashScreenActivity. The MvxFormsApp is basically empty. So we don't have to care. Example Code: https://github.com/smstuebe/stackoverflow-answers/tree/master/mvx-android-test-app
Create a nunit Test app project
Install-Package MvvmCross.StarterPack -Version 4.1.4
Get rid of Views folder
Install the SQLite plugin
Reference your Core project
Install-Package MvvmCross.Forms.Presenter -Version 4.1.4
Remove MainLauncher = true from MainActivity
Adust Setup to return your core project's App
protected override IMvxApplication CreateApp()
{
return new MyApp.Core.App();
}
Change SplashScreen to (source)
[Activity(MainLauncher = true
, Theme = "#style/Theme.Splash"
, NoHistory = true
, ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashScreen
: MvxSplashScreenActivity
{
public SplashScreen()
: base(Resource.Layout.SplashScreen)
{
}
private bool _isInitializationComplete;
public override void InitializationComplete()
{
if (!_isInitializationComplete)
{
_isInitializationComplete = true;
StartActivity(typeof(MainActivity));
}
}
protected override void OnCreate(Android.OS.Bundle bundle)
{
Forms.Init(this, bundle);
Forms.ViewInitialized += (object sender, ViewInitializedEventArgs e) =>
{
if (!string.IsNullOrWhiteSpace(e.View.StyleId))
{
e.NativeView.ContentDescription = e.View.StyleId;
}
};
base.OnCreate(bundle);
}
}
Write a test like
[TestFixture]
public class TestClass
{
[Test]
public void TestMethod()
{
var service = Mvx.Resolve<IDataStorageService>();
Assert.IsNull(service.UserData);
}
}
Enjoy the awesomeness of MvvmCross

Why is the Seed method called again in my EF code first migrations scenario?

I have a EF code first project and there is how I seed the database
internal sealed class Configuration : DbMigrationsConfiguration<myDB>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
ContextKey = "myDB.Auth.Service.DAL.myDB";
}
protected override void Seed(myDBdb)
{
var mProduct = new Product
{
Name = "default product",
CreatedDate = DateTime.Now
};
db.Products.AddOrUpdate(mProduct);
db.SaveChanges();
}
}
I have a wcf service that uses above code. What I realise is that every time I restart the wcf service (either from visual studio or IIS), above code is get called. As a result, multiple "default product" are added into the database, anyone knows why that happened?
Migration seed runs after every update-database so you need to make your script idempotent by testing for existance or using AddOrUpdate. If you only want to seed on database creation, there is a separate context seed method that only runs when the database is created.
https://blog.oneunicorn.com/2013/05/28/database-initializer-and-migrations-seed-methods/
AddOrUpdate for seeding
Edit:
When you use MigrateDatabaseToLatestVersion initializer, your seed method runs every time your application runs. If you want to control this process, switch your initializer to null:
Database.SetInitializer(new NullDatabaseInitializer<ApplicationDbContext>());
And then just manually run migrations when needed. To take it a step further, you can write your own initializer and do what you want when either the database does not exist or the database needs updating:
Database.SetInitializer(new ValidateDbInitializer<ApplicationDbContext>());
// ref: https://coding.abel.nu/2012/03/prevent-ef-migrations-from-creating-or-changing-the-database/
public class ValidateDbInitializer<TContext> : IDatabaseInitializer<TContext>
where TContext : ApplicationDbContext
{
public void InitializeDatabase(TContext context)
{
if (!context.Database.Exists())
{
throw new InvalidOperationException("The database does not exist. Check your server and connection string.");
}
if (!context.Database.CompatibleWithModel(true))
{
throw new InvalidOperationException("The database is not up to date. You may need to apply update(s).");
}
}
}
First step is to use the Tools menu, select Library Package Manager, then select Package Manager Console. In the Package Manager Console window type the below command.
Enable-Migrations
which will adds folder named as Migrations in your project and also a code file called as Configuration.cs.
in Configuration.cs type the below line
using yourprojectname.Models;
protected override void Seed(yourprojectname.Models.MyServiceContext context)
{
context.MyDB.AddOrUpdate(x => x.Id,
new MyData() { Name = "Mohit", CreatedDate= "14/05/2016" },
new MyData() { Name = "Prabhat", CreatedDate= "15/05/2016" },
);
}
Now type Update-Database
in Package Manager Console window
Try the following:
protected override void Seed(myDBdb)
{
var mProduct = new Product
{
Id = 1,
Name = "default product",
CreatedDate = DateTime.Now
};
db.Products.AddOrUpdate(mProduct);
db.SaveChanges();
}
When you are using the application for initialization the Data for the first time, please use DropCreateDatabaseAlways. e.g. :
public class MyClass : DropCreateDatabaseAlways<connectionstringContextName>
{
protected override void Seed(MyContext context)
{
// Your seed data
}
}

Eclipse Scout Neon mock backend service

In our project I have modules scout.client, scout.server, scout.shared and backend.
Backend has no dependencies to scout.server and scout.shared, but scout.server has dependencies to backend.
Inside backend project I have all business logic and calling all outside services.
My problem is when I try to test scout services that use some service from backend.
Because scout provide some great tool for mocking beans, we defined our service inside backend as beans as :
BEANS.getBeanManager().registerClass(CarService.class);
BEANS.getBeanManager().registerClass(PartnerService.class);
Both, CarService.class and PartnerService.class are in backend.
When I try to write some tests and I add #BeanMock to service in test
#BeanMock
private IPartnerService partnerService;
I get mock, but then every return every function is null, even if I write
doReturn(PartnerBuilder.standardPartnerListWithOneElement()).when(this.partnerService)
.getPartners(any(Set.class));
If I debug in my test, before this test is called with debugger I can get :
partnerService.getPartners(...) -> return a list of person
what is right, but when class that is tested calles this service it return null.
I understand that this could be due to missing annotation on interface #ApplicationScoped. Without this there is no guarantee that only one bean is created, and when statement react on another copy of that bean...?
I could not add annotation on interface because backend has no dependencies to scout modules.
How could I handle this kind of cases?
Tested class is :
public class UtilityPartner {
/**
* Method return service bean for getting partners by ids.
*
* #return
*/
private static IPartnerService getPartnerService() {
return BEANS.get(IPartnerService.class);
}
public static String getPartnerName(final Long partnerId) {
if (partnerId == null) {
return "";
}
final List<Partner> partners =
(List<Partner>) getPartnerService().getPartners(Sets.newHashSet(partnerId));
if (partners == null || partners.isEmpty()) {
return "";
}
final Partner partner = partners.get(0);
return LookupUtil.createLookupDescription(partner.getId(), partner.getName());
}
}
test class is :
#RunWith(ServerTestRunner.class)
#RunWithSubject("anonymous")
#RunWithServerSession(ServerSession.class)
public class TestUtilityPartner {
#BeanMock
private IPartnerService partnerService;
#Before
public void init() {
doReturn(PartnerBuilder.standardPartnerListWithOneElement()).when(this.partnerService).getPartners(any(Set.class));
}
#Test
public void getPartnerName() {
final String name = UtilityPartner.getPartnerName(10L);
Assert.assertEquals("My name", name); // NAME IS ""
}
}
Using #BeanMock does not help here, because you are not using an application scoped service:
In the init method you are changing the local field partnerService. However, in your test you call UtilityPartner.getPartnerService, which is creating a new instance (with BEANS.get(IPartnerService.class)).
#BeanMock is more useful for convenience for mocking application scoped beans.
You can always register your beans manually as shown by Jmini. Please do not forget to unregister the bean again after the test!
We recommend using org.eclipse.scout.rt.testing.shared.TestingUtility.registerBean(BeanMetaData), which is automatically adding a testing order and removing #TunnelToServer annotations.
I think that you should register your mock instance in the Bean manager (See bean registration in the Scout Architecture Document). You should use a small order (-10 000 is recommended for tests), in order for your mock to win over the productive registration. The best approach is to use the TestingUtility class to register/unregister your mock. Do not forget to call the unregisterBean() method (in the method annotated with #After):
import java.util.Collections;
import org.eclipse.scout.rt.platform.BeanMetaData;
import org.eclipse.scout.rt.platform.IBean;
import org.eclipse.scout.rt.testing.shared.TestingUtility;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
public class TestUtilityPartner {
private IBean<?> beanRegistration;
#Before
public void init() {
partnerService = Mockito.mock(IPartnerService.class);
// Register the mock using the Bean meta information:
BeanMetaData beanData = new BeanMetaData(IPartnerService.class)
.withInitialInstance(partnerService)
.withApplicationScoped(true);
this.beanRegistration = TestingUtility.registerBean(beanData);
// Mockito behavior:
Mockito.doReturn(Collections.singletonList(new Partner(34L, "John Smith")))
.when(partnerService).getPartners(Mockito.any(Set.class));
}
#After
public void after() {
// Unregister the mocked services:
TestingUtility.unregisterBean(this.beanRegistration);
}
#Test
public void getPartnerName() {
String name = UtilityPartner.getPartnerName(10L);
Assert.assertEquals("10 - John Smith", name);
}
}
I am not sure what #BeanMock (org.eclipse.scout.rt.testing.platform.mock.BeanMock) is doing, but according to Judith Gull's answer it will not work:
Using #BeanMock does not help here, because you are not using an application scoped service:
In the init method you are changing the local field partnerService. However, in your test you call UtilityPartner.getPartnerService, which is creating a new instance (with BEANS.get(IPartnerService.class)).
#BeanMock is more useful for convenience for mocking application scoped beans.

Resources