Skip to content

Instantly share code, notes, and snippets.

@7kashif
Last active February 10, 2022 07:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 7kashif/6f8084c585e3dc467b7f0ff05751d220 to your computer and use it in GitHub Desktop.
Save 7kashif/6f8084c585e3dc467b7f0ff05751d220 to your computer and use it in GitHub Desktop.
*****Notes on Testing*****
Why we need it:-
It takes a lot of time and effort to test every functionality in our software manually. And every time we are about to scale our
project we'd have to run our software and test each and every functionality manually to check whether it is still working correctly or not.
To overcome this effort we write Test Cases for our app so that with single click we can check anytime that whether a given piece of code is working correctly or not.
We get JUnit out of the box in our android project to test our code with a single click whenever we want to.
There are basically three types of test cases:-
-> Unit Tests:-
They make the bases of testing and make up to almost 70% of test cases in our app. They are used to test single Units or components of our app like any e.g: Testing the functionality of a class.
->Integration Tests:-
They tests whether two components of our software are working together correctly or not. E.g; Whether fragment and a viewModel are working together correctly or not. They make up to almost 20% of the testing.
->UI Tests:-
These tests check whether many or all components of our app are working together correctly and the UI is behaving like it should be. E.g; A checkbox which was supposed to be checked is checked or not. They are very large test cases and make up the rest of 10% test cases.
*****Test driven Development*****
Main principle:-
Write the test cases before the implementation of the function(only in case of Unit Tests).
-> First we write the signature of the function but no implementation of it.
-> Then we write the test case for this function and at this point the test must fail because we have not written any implementation of the function yet.
-> Then we write the implementation of our function so the test passes.
The assertions in our test cases should be kept minimum (only one if possible). Assertion means e.g; a value should not be null, or value must be true or a value must be positive. If these are more then one then it will make it harder for to know why exactly our test failed.
A test case has three characteristics:-
-> Scope:-
How much of the code this particular test case is testing.
-> Speed:-
A test case must run very quickly so that we can test our software frequently.
-> Fidelity:-
How close our test is to a real scenario. Because that is what we want to verify that our app works well in real world.
A test should not be flaky. Succeeding sometimes and failing the other times.
A test case should not depend upon the outcome of any other test case.
Test cases shoudld be minimum but enough to cover all aspects.
*****In Android*****
In android we get two code source directories. One with (androidTest) and other with(test)
All of our test that does not rely on any android component such as context, fragment etc, should be added in (test) directory.
Others that do depend on android components should be kept in (androidTest) directory.
We use following dependencies for testing in android:-
testImplementation 'junit:junit:4.13.2' //this one is used for unit testing
androidTestImplementation 'androidx.test.ext:junit:1.1.3' //this one is extension functions for junit
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' //it is used for UI testing
testImplementation 'com.google.truth:truth:1.1.3' //this one is used for easier assertions
****Example****
Lets say we want to test whether our fibonnacci function will give us the correct result or not.
First we will right only the signatur of the function,
fun fib(n: Int): Long {
return 0L
}
Now this function will return 0 in every input case.
Now write test cases for both when input is 0 and when input is greate than 0
@Test //this annotation is used to tell the compiler to test this function
fun `test fib function with input 0`(){
val result = RegistrationUtil.fib(0)
assertThat(result).isEqualTo(0)
}
@Test
fun `test fib function with value greater than 0`() {
val result = RegistrationUtil.fib(2)
assertThat(result).isEqualTo(1)
}
Now first test case will succeed but 2nd one will fail because in fabonnaci sequence the 2nd term is 1 but our function is return 0.
Now write the implementation of the fib(n) function.
fun fib(n: Int): Long {
if (n == 0 || n == 1)
return n.toLong()
var a = 0L
var b = 1L
var c = 0L
(2..n).forEach { _ ->
a = b + c
c = b
b = a
}
return a
}
Now our both of the test cases will succeed.
******Instrumented Unit Test******
These are the test that require an android component like context or fragment etc.
To run these test case we must connect a device or install emulator.
androidTestImplementation "com.google.truth:truth:1.1.3"
We need to add truth library with android test to use it for easier assertions in android test cases.
Let's say we have a class:-
class ResourceCompares {
/**
* this function will check whether the otherResString is same as the one
* we have in our resources.
*/
fun compareResources(context: Context, resId: Int, otherResString: String): Boolean {
return context.getString(resId) == otherResString
}
}
Here I have written the implementation because you already know the concept.
Tests for this class will be added in (androidTest) directory.
Now let's get to test cases.
class ResourceComparesTest {
/**
* private val resComp = ResourceCompares()
* as we are now testing a class so now we have to initialize it.
* first we can use this convention but it is bad because it will use the same object for every
* test case we write and for example if we have a counter variable that is changing values in different function
* inside the class our test cases will become flaky.
*/
/**
* private lateinit var resComp: ResourcesCompares()
* or we can use this approach and initialize it for every test case locally
* and that will slow down our test writing if we have number of functions
*/
//Best approach
private lateinit var resComp: ResourceCompares
@Before //now this function will initialize this object for us before every test case
fun setUp() {
resComp = ResourceCompares()
}
/**
* we use @After to destroy the object after every test case and create
* a new object for the next case.
* If using the same object could cause flakiness.
*/
@Test
fun stringResSameAsOtherStringRes_returnsTrue() {
val context = ApplicationProvider.getApplicationContext<Context>()
val result = resComp.compareResources(context, R.string.app_name, "Testing_example")
assertThat(result).isTrue()
}
@Test
fun stringResSDifferentFromOtherStringRes_returnsFalse() {
val context = ApplicationProvider.getApplicationContext<Context>()
val result = resComp.compareResources(context, R.string.app_name, "random_String")
assertThat(result).isFalse()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment