如何在JUnit4检测测试中测试领域存储库?

时间:2016-09-01 22:59:52

标签: android realm

在通过JUnit4工具的Android设备上,我想测试我的Realm类。

但我得到java.lang.IllegalStateException: Your Realm is opened from a thread without a Looper. Async queries need a Handler to send results of your query

private final Realm realm; //this is constructed on the instrumentation thread during testing, but in production it's constructed on the main thread.

public Observable<LocalCart> getCart() {
    return realm.where(RealmCart.class)
            .equalTo(RealmCart.DONE, true)
            .findAllAsync()  //throws thread w/o a looper exception, bc instrumentation thread does not have a looper
            .asObservable()
            .map(RealmResults::first)
            .map(LocalCart::create);
}

如何轻松实现目标?我的测试:

    cartDb = new CartDbImpl(dataTestDelegate.realm());

    cartDb.write(expected);

    TestSubscriber<LocalCart> test = new TestSubscriber<>();
    cartDb.getCart()
            .subscribe(test); //exception thrown here

    Assertions.assertThat(test).hasReceivedValues(expected);

1 个答案:

答案 0 :(得分:1)

两种解决方案:

1。)在您的应用程序中用您的测试中的inMemory() Realm替换您的Realm配置

@Before
public void setup() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    instrumentation.runOnMainSync(new Runnable() {
        @Override
        public void run() {
            ApplicationComponent applicationComponent = Injector.INSTANCE.getApplicationComponent();
            AppConfig appConfig = applicationComponent.appConfig();
            CustomApplication customApplication = applicationComponent.application();
            appConfig.setDefaultRealmConfig(new RealmConfiguration.Builder(customApplication).inMemory()
                    .deleteRealmIfMigrationNeeded()
                    .build());
            if(customApplication.getRealm() != null && !customApplication.getRealm().isClosed()) {
                customApplication.getRealm().close();
            }
            customApplication.initializeRealm();
        }
    });

然后使用

Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
instrumentation.runOnMainSync(() -> {
   // ...
});

2.)尝试以某种方式使这些后续类工作,我刚刚从Realm库检测测试中获取。我跟1)因为我在一段时间后放弃了它,但我确信它对Realm有用,所以它不应该是不可能的。

/*
 * Copyright 2014 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm;

import android.os.Looper;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static junit.framework.Assert.fail;

public class TestHelper {
    // Returns a random key used by encrypted Realms.
    public static byte[] getRandomKey() {
        byte[] key = new byte[64];
        new Random().nextBytes(key);
        return key;
    }

    // Returns a random key from the given seed. Used by encrypted Realms.
    public static byte[] getRandomKey(long seed) {
        byte[] key = new byte[64];
        new Random(seed).nextBytes(key);
        return key;
    }

    // Alloc as much garbage as we can. Pass maxSize = 0 to use it.
    public static byte[] allocGarbage(int garbageSize) {
        if(garbageSize == 0) {
            long maxMemory = Runtime.getRuntime().maxMemory();
            long totalMemory = Runtime.getRuntime().totalMemory();
            garbageSize = (int) (maxMemory - totalMemory) / 10 * 9;
        }
        byte garbage[] = new byte[0];
        try {
            if(garbageSize > 0) {
                garbage = new byte[garbageSize];
                garbage[0] = 1;
                garbage[garbage.length - 1] = 1;
            }
        } catch(OutOfMemoryError oom) {
            return allocGarbage(garbageSize / 10 * 9);
        }

        return garbage;
    }

    // Creates SHA512 hash of a String. Can be used as password for encrypted Realms.
    public static byte[] SHA512(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            md.update(str.getBytes("UTF-8"), 0, str.length());
            return md.digest();
        } catch(NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch(UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public static void awaitOrFail(CountDownLatch latch) {
        awaitOrFail(latch, 30);
    }

    public static void awaitOrFail(CountDownLatch latch, int numberOfSeconds) {
        try {
            if(!latch.await(numberOfSeconds, TimeUnit.SECONDS)) {
                fail("Test took longer than " + numberOfSeconds + " seconds");
            }
        } catch(InterruptedException e) {
            fail(e.getMessage());
        }
    }

    // clean resource, shutdown the executor service & throw any background exception
    public static void exitOrThrow(final ExecutorService executorService, final CountDownLatch signalTestFinished, final CountDownLatch signalClosedRealm, final Looper[] looper, final Throwable[] throwable)
            throws Throwable {

        // wait for the signal indicating the test's use case is done
        try {
            // Even if this fails we want to try as hard as possible to cleanup. If we fail to close all resources
            // properly, the `after()` method will most likely throw as well because it tries do delete any Realms
            // used. Any exception in the `after()` code will mask the original error.
            TestHelper.awaitOrFail(signalTestFinished);
        } finally {
            // close the executor
            executorService.shutdownNow();
            if(looper[0] != null) {
                // failing to quit the looper will not execute the finally block responsible
                // of closing the Realm
                looper[0].quit();
            }

            // wait for the finally block to execute & close the Realm
            TestHelper.awaitOrFail(signalClosedRealm);

            if(throwable[0] != null) {
                // throw any assertion errors happened in the background thread
                throw throwable[0];
            }
        }
    }

    public static abstract class Task {
        public abstract void run()
                throws Exception;
    }

    public static void executeOnNonLooperThread(final Task task)
            throws Throwable {
        final AtomicReference<Throwable> thrown = new AtomicReference<Throwable>();
        final Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    task.run();
                } catch(Throwable e) {
                    thrown.set(e);
                    if(e instanceof Error) {
                        throw (Error) e;
                    }
                }
            }
        };
        thread.start();
        thread.join();

        final Throwable throwable = thrown.get();
        if(throwable != null) {
            throw throwable;
        }
    }
}

/*
 * Copyright 2016 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm.rule;

import android.content.Context;
import android.content.res.AssetManager;

import org.junit.rules.TemporaryFolder;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import io.realm.Realm;
import io.realm.RealmConfiguration;

import static org.junit.Assert.assertTrue;

/**
 * Rule that creates the {@link RealmConfiguration } in a temporary directory and deletes the Realm created with that
 * configuration once the test finishes. Be sure to close all Realm instances before finishing the test. Otherwise
 * {@link Realm#deleteRealm(RealmConfiguration)} will throw an exception in the {@link #after()} method.
 * The temp directory will be deleted regardless if the {@link Realm#deleteRealm(RealmConfiguration)} fails or not.
 */
public class TestRealmConfigurationFactory
        extends TemporaryFolder {
    private Map<RealmConfiguration, Boolean> map = new ConcurrentHashMap<RealmConfiguration, Boolean>();
    private Set<RealmConfiguration> configurations = Collections.newSetFromMap(map);
    protected boolean unitTestFailed = false;

    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate()
                    throws Throwable {
                before();
                try {
                    base.evaluate();
                } catch(Throwable throwable) {
                    unitTestFailed = true;
                    throw throwable;
                } finally {
                    after();
                }
            }
        };
    }

    @Override
    protected void before()
            throws Throwable {
        super.before();
    }

    @Override
    protected void after() {
        try {
            for(RealmConfiguration configuration : configurations) {
                Realm.deleteRealm(configuration);
            }
        } catch(IllegalStateException e) {
            // Only throw the exception caused by deleting the opened Realm if the test case itself doesn't throw.
            if(!unitTestFailed) {
                throw e;
            }
        } finally {
            // This will delete the temp folder.
            super.after();
        }
    }

    public RealmConfiguration createConfiguration() {
        RealmConfiguration configuration = new RealmConfiguration.Builder(getRoot()).build();

        configurations.add(configuration);
        return configuration;
    }

    public RealmConfiguration createConfiguration(String subDir, String name) {
        final File folder = new File(getRoot(), subDir);
        assertTrue(folder.mkdirs());
        RealmConfiguration configuration = new RealmConfiguration.Builder(folder).name(name).build();

        configurations.add(configuration);
        return configuration;
    }

    public RealmConfiguration createConfiguration(String name) {
        RealmConfiguration configuration = new RealmConfiguration.Builder(getRoot()).name(name).build();

        configurations.add(configuration);
        return configuration;
    }

    public RealmConfiguration createConfiguration(String name, byte[] key) {
        RealmConfiguration configuration = new RealmConfiguration.Builder(getRoot()).name(name).encryptionKey(key).build();

        configurations.add(configuration);
        return configuration;
    }

    public RealmConfiguration.Builder createConfigurationBuilder() {
        return new RealmConfiguration.Builder(getRoot());
    }

    // Copies a Realm file from assets to temp dir
    public void copyRealmFromAssets(Context context, String realmPath, String newName)
            throws IOException {
        // Delete the existing file before copy
        RealmConfiguration configToDelete = new RealmConfiguration.Builder(getRoot()).name(newName).build();
        Realm.deleteRealm(configToDelete);

        AssetManager assetManager = context.getAssets();
        InputStream is = assetManager.open(realmPath);
        File file = new File(getRoot(), newName);
        FileOutputStream outputStream = new FileOutputStream(file);
        byte[] buf = new byte[1024];
        int bytesRead;
        while((bytesRead = is.read(buf)) > -1) {
            outputStream.write(buf, 0, bytesRead);
        }
        outputStream.close();
        is.close();
    }
}

/*
 * Copyright 2015 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm.rule;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * This annotation should be used along with {@link RunInLooperThread}
 * When the annotation is present, the test method is executed on a worker thread with a looper.
 * This will also uses {@link org.junit.rules.TemporaryFolder} to create and open a Realm.
 * Annotation param {@link io.realm.rule.RunInLooperThread.RunnableBefore} can be supplied which will run before the
 * looper thread.
 */
@Target(METHOD)
@Retention(RUNTIME)
public @interface RunTestInLooperThread {
    Class<? extends RunInLooperThread.RunnableBefore> value() default RunInLooperThread.RunnableBefore.class;
}

/*
 * Copyright 2015 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm.rule;

import android.os.Handler;
import android.os.Looper;

import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.TestHelper;

import static org.junit.Assert.fail;

/**
 * Rule that runs the test inside a worker looper thread. This rule is responsible
 * of creating a temp directory containing a Realm instance then delete it, once the test finishes.
 *
 * All Realms used in a method method annotated with {@code @RunTestInLooperThread } should use
 * {@link RunInLooperThread#createConfiguration()} and friends to create their configurations. Failing to do so can
 * result in the test failing because the Realm could not be deleted (Reason is that {@link TestRealmConfigurationFactory}
 * and this class does not agree in which order to delete all open Realms.
 */
public class RunInLooperThread
        extends TestRealmConfigurationFactory {
    public Realm realm;
    public RealmConfiguration realmConfiguration;
    private CountDownLatch signalTestCompleted;
    private Handler backgroundHandler;

    // the variables created inside the test are local and eligible for GC.
    // but sometimes we need the variables to survive across different Looper
    // events (Callbacks happening in the future), so we add a strong reference
    // to them for the duration of the test.
    public LinkedList<Object> keepStrongReference;

    @Override
    protected void before()
            throws Throwable {
        super.before();
        realmConfiguration = createConfiguration(UUID.randomUUID().toString());
        signalTestCompleted = new CountDownLatch(1);
        keepStrongReference = new LinkedList<Object>();
    }

    @Override
    protected void after() {
        super.after();
        realmConfiguration = null;
        realm = null;
        keepStrongReference = null;
    }

    @Override
    public Statement apply(final Statement base, Description description) {
        final RunTestInLooperThread annotation = description.getAnnotation(RunTestInLooperThread.class);
        if(annotation == null) {
            return base;
        }
        return new Statement() {
            private Throwable testException;

            @Override
            public void evaluate()
                    throws Throwable {
                before();
                Class<? extends RunnableBefore> runnableBefore = annotation.value();
                if(!runnableBefore.isInterface()) {
                    runnableBefore.newInstance().run(realmConfiguration);
                }
                try {
                    final CountDownLatch signalClosedRealm = new CountDownLatch(1);
                    final Throwable[] threadAssertionError = new Throwable[1];
                    final Looper[] backgroundLooper = new Looper[1];
                    final ExecutorService executorService = Executors.newSingleThreadExecutor();
                    executorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            Looper.prepare();
                            backgroundLooper[0] = Looper.myLooper();
                            backgroundHandler = new Handler(backgroundLooper[0]);
                            try {
                                realm = Realm.getInstance(realmConfiguration);
                                base.evaluate();
                                Looper.loop();
                            } catch(Throwable e) {
                                threadAssertionError[0] = e;
                                unitTestFailed = true;
                            } finally {
                                try {
                                    looperTearDown();
                                } catch(Throwable t) {
                                    if(threadAssertionError[0] == null) {
                                        threadAssertionError[0] = t;
                                    }
                                    unitTestFailed = true;
                                }
                                if(signalTestCompleted.getCount() > 0) {
                                    signalTestCompleted.countDown();
                                }
                                if(realm != null) {
                                    realm.close();
                                }
                                signalClosedRealm.countDown();
                            }
                        }
                    });
                    TestHelper.exitOrThrow(executorService, signalTestCompleted, signalClosedRealm, backgroundLooper, threadAssertionError);
                } catch(Throwable error) {
                    // These exceptions should only come from TestHelper.awaitOrFail()
                    testException = error;
                } finally {
                    // Try as hard as possible to close down gracefully, while still keeping all exceptions intact.
                    try {
                        after();
                    } catch(Throwable e) {
                        if(testException != null) {
                            // Both TestHelper.awaitOrFail() and after() threw an exception. Make sure we are aware of
                            // that fact by printing both exceptions.
                            StringWriter testStackTrace = new StringWriter();
                            testException.printStackTrace(new PrintWriter(testStackTrace));

                            StringWriter afterStackTrace = new StringWriter();
                            e.printStackTrace(new PrintWriter(afterStackTrace));

                            StringBuilder errorMessage = new StringBuilder().append("after() threw an error that shadows a test case error")
                                    .append('\n')
                                    .append("== Test case exception ==\n")
                                    .append(testStackTrace.toString())
                                    .append('\n')
                                    .append("== after() exception ==\n")
                                    .append(afterStackTrace.toString());
                            fail(errorMessage.toString());
                        } else {
                            // Only after() threw an exception
                            throw e;
                        }
                    }

                    // Only TestHelper.awaitOrFail() threw an exception
                    if(testException != null) {
                        //noinspection ThrowFromFinallyBlock
                        throw testException;
                    }
                }
            }
        };
    }

    /**
     * Signal that the test has completed.
     */
    public void testComplete() {
        signalTestCompleted.countDown();
    }

    /**
     * Signal that the test has completed.
     *
     * @param latches additional latches to wait before set the test completed flag.
     */
    public void testComplete(CountDownLatch... latches) {
        for(CountDownLatch latch : latches) {
            TestHelper.awaitOrFail(latch);
        }
        signalTestCompleted.countDown();
    }

    /**
     * Posts a runnable to this worker threads looper.
     */
    public void postRunnable(Runnable runnable) {
        backgroundHandler.post(runnable);
    }

    /**
     * Tear down logic which is guaranteed to run after the looper test has either completed or failed.
     * This will run on the same thread as the looper test.
     */
    public void looperTearDown() {
    }

    /**
     * If an implementation of this is supplied with the annotation, the {@link RunnableBefore#run(RealmConfiguration)}
     * will be executed before the looper thread starts. It is normally for populating the Realm before the test.
     */
    public interface RunnableBefore {
        void run(RealmConfiguration realmConfig);
    }
}